Creating a Desktop Two-Dimensional Game Using ggez

In the preceding chapter, we saw how to build interactive software based on the animation-loop architecture (typically, animated games) for desktops or for web browsers from a single set of source codes using the quicksilver framework. A drawback of this approach is that many input/output functions available on the desktop are not available on web browsers, and so a framework for web browsers does not necessarilyprovide as many features to desktop applications that are offered on desktop platforms, such as file storage.

In addition, when using the animation-loop architecture, it is quite awkward to get discrete input, such as mouse clicks, typed letters, or digits. For this, an event-driven architecture is more appropriate.

In this chapter, another application framework will be introduced—the ggez framework. This handles both animation-loop and discrete events, but at the time of writing, it onlysupports two-dimensional desktop applications.

In the previous chapter, we saw that to compute the position and orientation of various graphical objects, some analytical geometry and trigonometry is required. For more complex applications, these mathematical computations can become overwhelming. To simplify the code, it is useful to encapsulate positions in point objects and translations in vector objects, and so in this chapter, we will look at how to perform these encapsulations. The nalgebra mathematical library helps us to do this and will be introduced in this chapter, too.

The following topics will be covered in this chapter:

  • Understanding linear algebra
  • Implementing the gg_ski project
  • Implementing the gg_silent_slalom project
  • Implementing the gg_assets_slalom project
  • Implementing the gg_whac project

In particular, you will see the implementation of the same three projects we looked at in the previous chapter (gg_ski, gg_silent_slalom, and gg_assets_slalom) to demonstrate the animation loop, as well as a Whac-A-Mole game (gg_whac) to demonstrate the handling of discrete events.

Technical requirements

This chapter uses references to the animation-loop architecture and the slalom game implemented in the preceding chapter. The ggez framework requires (for correctly rendering graphical objects) the OpenGL 3.2 API to be well supported by the operating system. Therefore, old operating systems such as Windows XP cannot be used.

The complete source code for this chapter is found in the Chapter07folder of the repository at https://github.com/PacktPublishing/Creative-Projects-for-Rust-Programmers.

macOS users may struggle to install coreaudio-sys. Upgrading the patch version of coreaudio-sys to 0.2.3 resolves this issue.

Project overview

In this chapter, we will firstlook at what linear algebra isand why it is useful to describe and manipulate the objects drawn in any graphical game. Then, we will look at how to use thenalgebralibrary to perform linear algebra operations in our programs.

After that, we will recreate the same projects used in the previous chapter, but using the nalgebralibrary and the ggez framework instead of the quicksilver framework.gg_ski is a rewrite of ski, gg_silent_slalom is a rewrite of silent_slalom, and gg_assets_slalom is a rewrite of assets_slalom.

At the end of the chapter, we will look at the implementation of a completely different game with the gg_whacproject to see how to handle discrete events in an architecture that mixes the animation loop with an event-driven architecture. This will alsoshow how widgets (such as buttons) can be created and added to a window.

Understanding linear algebra

Linear algebra is the sector of mathematics regarding systems of first-degree equations, such as the following:

This system of equations has a solution to certain values (that is, ). In addition to being useful for solving systems of equations, the concepts and methods of linear algebra are also useful for representing and manipulating geometrical entities.

In particular, any position on a plane can be represented by two coordinates, x and y, and any position in space can be represented by three coordinates, x, y, and z. In addition, any translation of a position on a plane can be represented by two coordinates, Δx and Δy, and any translation of a position in space can be represented by three coordinates, Δx, Δy, and Δz.

For example, consider two positions on a plane:

  • p1: Its coordinates are x = 4, y = 7.
  • p2: Its coordinates are x = 10, y = 16.

Consider two translations on that plane:

  • t1: Its coordinates are .
  • t2: Its coordinates are .

You can say that if you translate the p1 position by the t1 translation, you get to the p2 position. The computation is done by adding the corresponding coordinates: p1x + t1x = p2x (or, in numbers, 4 + 6 = 10) and p1y+ t1y= p2y (or, in numbers, 7 + 9 = 16).

If you apply two translations sequentiallyto the p1position—the t1 translation and the t2 translation—then you will obtain another position (say, p3). You will also obtain the same result if you first sum the two translations (by summing their components memberwise) and then applying the resulting translation to p1.

So, for the x coordinate, we have (p1x + t1x) + t2x = p1x + (t1x + t2x) and a similar equation also holds for the y coordinate. So, translations can be added. You can add a translation to another one by summing their respective coordinates, instead, it does not make sense to add oneposition to anotherposition.

You can simplify your geometric computations by applying the computations to theposition and translation entities themselves using the following formula:

In linear algebra, there are two concepts that can be applied to these sorts of operations:

  • Vectors: An algebraic vector is a tuple of numbers that can be added to another vector, obtaining another vector, which is what is needed to represent translations.
  • Points: An algebraic point is a tuple of numbers that cannot be added to another point, but that can be incremented by a vector, thereby obtaining another point, which is what is needed to represent aposition.

Therefore, linear algebraic N-dimensional vectors are fit to represent translations in an N-dimensional geometric space, whereas linear algebraic N-dimensional points are fit to represent positions in an N-dimensional geometric space.

The nalgebra library (pronounced en-algebra) is a collection of to many algebraic algorithms that provide implementations for these kinds of two-dimensional point and vector types, and so it will be used in all of the following projects.

Using this library, you can write the following program, which shows which operations are allowed and which are forbidden, using vectors and points:

use nalgebra::{Point2, Vector2};
fn main() {
let p1: Point2<f32> = Point2::new(4., 7.);
let p2: Point2<f32> = Point2::new(10., 16.);
let v: Vector2<f32> = Vector2::new(6., 9.);

assert!(p1.x == 4.);
assert!(p1.y == 7.);
assert!(v.x == 6.);
assert!(v.y == 9.);

assert!(p1 + v == p2);
assert!(p2 - p1 == v);
assert!(v + v - v == v);
assert!(v == (2. * v) / 2.);

//let _ = p1 + p2;
let _ = 2. * p1;
}

The first three statements of the main function create two two-dimensional points and one two-dimensional vector whose coordinates are f32 numbers. This sort of inner numeric type can often be inferred, but here it is specified for clarity.

The next four statements show that both the Point2 and Vector2types contain the xandyfields, initialized by the arguments of thenewfunction. So, thePoint2andVector2types look quite similar, and actually many libraries and many developers use just one type to store bothpositions and translations.

However, these types differ for the allowed operations. The following four statements show which operations can be carried out:

  • Sum a vector to a point (p1 + v), obtaining another point.
  • Subtract two points (p2 - p1), obtaining a vector.
  • Sum two vectors or subtract two vectors (v + v - v), obtaining a vector in both cases.
  • Multiply a vector by a number or divide a vector by a number ((2. * v) / 2.), obtaining a vector in both cases.

There are some operations allowed on vectors that shouldn't be allowed on points (because they make no sense for them), which the last two statements show. You cannot add two points (p1 + p2) and actually, this operation is commented out to prevent a compilation error. You shouldn't multiply a point by a number (2. * p1), although, for some reason, the nalgebra library allows this.

If you want to learn more about the nalgebra library, you can find its documentation at https://www.nalgebra.org/.

Now that we have looked at a good way to handle geometric coordinates using the nalgebra library, let's see how to use them in game applications.

Implementing the gg_ski project

The first three projects in this chapter are just a rewrite of the three projects covered in the preceding chapter but are converted so that they use the ggez framework and the nalgebra library instead. They are as follows:

  • The ski project has become gg_ski.
  • The silent_slalom project has become gg_silent_slalom.
  • The assets_slalom project has become gg_assets_slalom.

Each project's behavior is very similar to its respective project in Chapter 6, Creating a WebAssembly Game Using Quicksilver, and so you can go back to that chapter to see the screenshots accompanying each one. For all three projects, gg_ski, gg_silent_slalom, and gg_assets_slalom, the Cargo.toml file has the following change. Instead of the quicksilver dependency, there are the following dependencies:

ggez = "0.5"
nalgebra = "0.18"

The term ggez (pronounced G. G. easy) is a slang term used by multiplayer online gamers.

The ggez framework was admittedlyinspired by the LÖVE game framework. The main difference between them lies in the programming languages. LÖVE is implemented in C++ and is programmable in Lua, whileggezis both implemented and programmable in Rust.

Now, let's compare the main.rs source code of the ski project to that of the gg_ski project.

The main function

At the end of the file, there is the main function, which prepares the context for the game and then runs the game:

fn main() -> GameResult {
let (context, animation_loop) = &mut ContextBuilder::new
("slalom", "ggez")
.window_setup(conf::WindowSetup::default().title("Slalom"))
.window_mode(conf::WindowMode::default().dimensions(SCREEN_WIDTH,
SCREEN_HEIGHT))
.add_resource_path("static")
.build()?;
let game = &mut Screen::new(context)?;
event::run(context, animation_loop, game)
}

In this function, you can see that, when you use the ggez framework, you don't just run the model. First, you should create three objects:

  • A context, which, in our case, is a window. It is assigned to the context variable.
  • An animation loop, which animates that context. It is assigned to the animation_loop variable.
  • The model, in our case, is of Screentype. It is assigned to the game variable.

After creating these objects, you can call the run function with these three objects as arguments.

To create the context and the animation loop, a ContextBuilder object is first created by calling the ContextBuilder::new function; then, this builder is modified by calling its methods—window_setup, window_mode, and add_resource_path. Finally, the call to the build method returns both a context and an animation loop.

However, notice the following things:

  • The call to new specifies a name for the app ("slalom") and a name for its creator ("ggez").
  • The call to window_setup specifies the text in the title bar of the window ("Slalom").
  • The call to window_mode specifies the desired size of the window.
  • The call to add_resource_path specifies the name of the folder that will contain the assets loaded at runtime ("static"), even if we are not going to use assets in this project.

Regarding the Screen model, notice that it is created using the new method, and so we will have to provide this method; however, we could use any other name for this sort of creation method.

Patterns of input handling

Both quicksilver and ggez adopt an animation loop-based Model-View-Controller (MVC) pattern. This is done by requiring the model to implement a trait that has two required methods:

  • update is the controller.
  • draw is the view.

Both frameworks run an implicit loop that periodically (many times per second) calls the following:

  • The controller to update the model, using possible input data and the preceding values of the model
  • The view to update the screen, using the updated values of the model

However, there is a substantial difference in the technique used by these frameworks to get input. quicksilver is a complete animation loop-oriented framework. The controller (or the update function) gets input accessing the state of input devices—it can check where the mouse is and which mouse buttons and keyboard keys are being pressed.

Instead, ggez input handling is event-driven because it captures input device transitions, not input device states. There are several kinds of possible input device transitions:

  • A movement of the mouse (mouse moves)
  • A press of a mouse button (mouse button down)
  • A release of a pressed mouse button (mouse button up)
  • A press of keyboard key (key down)
  • A release of a pressed keyboard key (key up)

In ggez, for each of these possible input device transitions, the trait declares an optional handler routine that can be implemented for the model by the application code. These routines are called mouse_motion_event, mouse_button_down_event, mouse_button_up_event, key_down_event, and key_up_event.

If an event happens in an animation-loop time frame, the corresponding handlers are invoked just before the update function is invoked. In these event handlers, the application code should store (in the model) the information gathered from the event, such as which key has been pressed or in which position the mouse has been moved. Then, the update function can process this input data to prepare the information needed by the view.

To better understand these techniques, consider, as an example, the following sequence of events or timeline:

  • The update function is invoked 10 times per second—that is, once every tenth of a second—so, frames per second = 10.
  • The user presses the A key at 0.020 seconds and releases it 50 milliseconds later at 0.070 seconds, and then they press the B key at 0.140 seconds and release it 240 milliseconds later at 0.380 seconds.

For quicksilver, we have the following timeline:

At time Input device state Input processing in theupdatefunction
0.0 No key is pressed. Nothing.
0.1 No key is pressed. Nothing.
0.2 The B key is pressed. The B key is processed.
0.3 The B key is pressed. The B key is processed.
0.4 No key is pressed. Nothing.
0.5 No key is pressed. Nothing.

For ggez, we have the following timeline:

At time Input events Input processing in the update function
0.0 No input events. No key info is stored in the model.
0.1

The key_down_event function is invoked with the A key as an argument. It stores the A key in the model.

The key_up_event function is invoked with the A key as an argument. It does nothing.

The A key is read from the model. It is processed and reset.
0.2 The key_down_event function is invoked with the B key as an argument. It stores the B key in the model. The B key is read from the model. It is processed and reset.
0.3 No input events. No key info is stored in the model.
0.4 The key_up_event function is invoked with the B key as an argument. It does nothing. No key info is stored in the model.
0.5 No input events. No key info is stored in the model.

Notice that for quicksilver, the A key has never been pressed, while the B key has been pressed twice. This can be good for continuous events, such as using a joystick, but not for discrete events, such as clicking a command button or typing text into a textbox.

However, quicksilver has the advantage of capturing all simultaneous events. For example, quicksilver can easily handlea chord, which is when several keys are continually pressed at the same time.

Instead, for ggez, as long as only one key is pressed in a time frame, all key presses are handled the appropriate number of times. This is expected for buttons and textboxes; however, chords are not handled correctly. The only key combinations handled by ggez are those involving the Shift, Ctrl, and Altspecial keys.

Input handling in the gg_ski project

Among the many events that can be captured by a ggez application, the gg_ski game captures only two events—the press of the right or left arrow keys and their release. The handling of these events stores the relevant input information in the model so that the update function can use it. Therefore, the model must contain some additional fields, with respect to those contained for the quicksilverski project.

So, we nowhave a model that contains some fields updated by the event functions, to be used by theupdatefunction, and some other fields updated by theupdatefunction, to be used by thedrawfunction. To distinguish these input fields, it's better to encapsulate them in a structure defined as follows:

struct InputState {
to_turn: f32,
started: bool,
}

The to_turn field indicates that the user has pressed an arrow key to change the direction of the ski. If only the left key is pressed, the direction angle should be decremented, and so the value of this field should be -1.0. If only the right key is pressed, the direction angle should be incremented, and so the value of this field should be 1.0. If the user has not pressed any arrow key, the direction should remain unchanged, and so the value of this field should be 0.0.

The started field indicates that the race has started. It is not used in this project. An instance of this structure is added to the model using the following line:

input: InputState,

The capture of key presses is done through the following code:

fn key_down_event(
&mut self,
_ctx: &mut Context,
keycode: KeyCode,
_keymod: KeyMods,
_repeat: bool,
) {
match keycode {
KeyCode::Left => { self.input.to_turn = -1.0; }
KeyCode::Right => { self.input.to_turn = 1.0; }
_ => (),
}
}

The keycode argument specifies which key has been pressed. If the left or the right arrow keys have been pressed, the to_turn field is set to -1.0 or to +1.0, respectively. Any other keys that are pressed are ignored.

Capturing the release of the keys is done through the following code:

fn key_up_event(&mut self, _ctx: &mut Context, keycode: KeyCode, _keymod: KeyMods) {
match keycode {
KeyCode::Left | KeyCode::Right => {
self.input.to_turn = 0.0;
}
_ => (),
}
}

If the left or the right arrow keys are released, the to_turn field is set to 0.0 to stop the change of direction. The release of any other key is ignored.

Other differences with quicksilver

Between quicksilver and ggez, in addition to the described conceptual differences, there are some minor differences, which I have covered in the following subsections.

Name of the trait

The name of the trait to be implemented by the model is State for quicksilver and EventHandler for ggez. So, for quicksilver we had the following line:

impl State for Screen {

But in ggez, we have the following:

impl EventHandler for Screen {

The type of context

Using both quicksilver and ggez, you need to implement the update method and the draw method. Both of these methods receive an argumentfor both frameworks that describes the input/output context. This context is the object used to receive interactive input (by the update method) and to emit graphical output (by the draw method).

However, for quicksilver the type of this context argument is Window, as in the following function signatures:

fn update(&mut self, window: &mut Window) -> Result<()> {
...
fn draw(&mut self, window: &mut Window) -> Result<()> {

For ggez, it is Context. So, now we have the following signatures:

fn update(&mut self, ctx: &mut Context) -> GameResult {
...
fn draw(&mut self, ctx: &mut Context) -> GameResult {

The new method

The State trait of quicksilver requires the implementation of the new method, used by the framework to create the model instance. The EventHandler trait of ggez has no such method because the model instance is created explicitly by application code in the main function, as we have seen.

The angle's unit of measurement

While quicksilver rotation angles must be specified in degrees, ggez rotation angles must be specified in radians, and so angular constants and variables are specified in these units of measurement. So, now we have the following lines:

const STEERING_SPEED: f32 = 110. / 180. * PI; // in radians/second
const MAX_ANGLE: f32 = 75. / 180. * PI; // in radians

How to specify the FPS rate

To specify the desiredFrames Per Second (FPS) rate, two parameters are specified in themainfunctionwhen using quicksilver, whereas ggez uses another technique. For ggez, theupdatefunction is always invoked 60 times per second (if possible), but the application code can simulate a different rate by writing the following body of theupdatefunction:

const DESIRED_FPS: u32 = 25;
while timer::check_update_time(ctx, DESIRED_FPS) {
...
}

The purpose of this code is to ensure that the body of thewhile loop is executed with the specified rate, which in this case is 25 frames per second. Let's see how this is accomplished.

The required ratespecified in our code means that the body should be executed once every 1000 / 25 = 40 milliseconds. When theupdatefunction is executed, if less than 40 milliseconds have elapsed since the preceding execution, thecheck_update_timefunction returnsfalse, and so the body of the while loop is not executed this time. It is likely that even at the next update call, not enough time will have elapsed, and so thecheck_update_timefunction will returnfalseagain. In a later execution, when at least 40 milliseconds have elapsed since the last time the body was executed,true will be returned, and so the body will be executed.

This allows a rate that is lower than 60 FPS. However, there is another feature. If a frame, for some reason, takes longer than the allotted time—say, 130 milliseconds—causing the animation to stutter, then thecheck_update_timefunction returnstrueseveral times in a row to make up for the lost time.

Of course, you cannot obtain the desired rate if every frame is so slow to take too much time. Tough, as long as your frames are processed within the required time, this technique ensures that the average frame rate will be the specified one.

To say that the actual average rate approximates the desired rate, it is enough that the average time taken by a frame is less than the one allotted for a frame.Instead, if your frames take, on average, 100 milliseconds, the actual frame rate will be 10 FPS.

Handling the ski steering

The ski steering is handled differently in the body of theupdateloop. In theski project, thesteerfunction is onlycalled if an arrow key is kept pressed down at that time. Instead, in thegg_skyproject, the following statement is always executed:

self.steer(self.input.to_turn);

The steer function is called at any time frame, passing the value set before by the input handling methods. If this value is 0, the ski doesn't steer.

Computation of new position and speed

In addition, the body of the update function now contains the following statements:

let now = timer::time_since_start(ctx);
self.period_in_sec = (now - self.previous_frame_time)
.as_millis() as f32 / 1000.;
self.previous_frame_time = now;

Their purpose is to compute the correct kinematics of the ski. In mechanics, to compute a position variation (), you have to multiply the current speed (also called velocity, v) by the time elapsed since the previous frame (). This results in the following equation:

To compute a speed variation (), you have to multiply the current acceleration (a) by the time elapsed since the preceding frame (), which results in the following equation:

So, to compute the position variation and the speed variation, we need the time elapsed since the preceding frame. The ggez framework provides the timer::time_since_start function, which returns the duration since the start of the application. We subtract the time of the preceding frame from the duration to obtain the time elapsed between the two frames. The duration is then converted into seconds. Finally, the current time is saved, to be used in the next frame computation.

Drawing the background

The MVC view implemented by the draw method draws the white background by using the following statement:

graphics::clear(ctx, graphics::WHITE);

Now, let's check how to draw the composite shapes.

Drawing composite shapes

To draw a composite shape, instead of individually drawing all of itscomponents, first create aMesh object, which is a composite shape containing all the component shapes, and then draw the Mesh object. To create aMesh object, theMeshBuilderclass is used with this code:

let ski = graphics::MeshBuilder::new()
.rectangle(
DrawMode::fill(),
Rect {
x: -SKI_WIDTH / 2.,
y: SKI_TIP_LEN,
w: SKI_WIDTH,
h: SKI_LENGTH,
},
[1., 0., 1., 1.].into(),
)
.polygon(
DrawMode::fill(),
&[
Point2::new(-SKI_WIDTH / 2., SKI_TIP_LEN),
Point2::new(SKI_WIDTH / 2., SKI_TIP_LEN),
Point2::new(0., 0.),
],
[0.5, 0., 1., 1.].into(),
)?
.build(ctx)?;

Let's now check what this code does:

  1. First, thenewfunction creates a MeshBuilderobject.
  2. Then, the methods instruct these mesh builders how to create the mesh components. Therectanglemethod explains how to create a rectangle, which will be the ski body, and the polygon method explains how to create a polygon, which will be the ski tip. The features of the rectangle are its draw mode (DrawMode::fill()), its position and size (x, y, w, and h), and its color (1., 0., 1., 1.). The features of the polygon are its draw mode, the list of its vertices, and its color. It has just three vertices, so it is a triangle.
  3. Then, the buildmethod creates and returns the specified mesh. Notice that the method calls ending with a question mark are fallible and that the colors are specified by the quadruple red-green-blue-alpha model, where each number is in the range 0 to 1.

To draw a mesh, the following statement is used:

graphics::draw(
ctx,
&ski,
graphics::DrawParam::new()
.dest(Point2::new(
SCREEN_WIDTH / 2. + self.ski_across_offset,
SCREEN_HEIGHT * 15. / 16. - SKI_LENGTH / 2.
- SKI_TIP_LEN,
))
.rotation(self.direction),
)?;

This draw method is not the same as the draw method that defines the view of the MVC architecture. This is found in the ggez::graphics module, while the containing method (the view) is part of the ggez::event::EventHandler trait.

The first argument of the graphics::draw method—ctx—is the context on which we are drawing. The second argument—&ski—is the mesh we are drawing. The third argument is a collection of parameters, encapsulated in a DrawParam object. This type allows us to specify numerous parameters, two of which are specified as follows:

  • The point to draw the mesh, specified using the dest method
  • The angle (in radians) by which the mesh must be rotated, specified using the rotation method

So, we have now seen how to draw on the screen. However, after calling these statements, nothing actually appears on the screen because the statements just prepare the output off-screen. To get the output, a finalization statement is needed, which is described in the next section.

Ending the draw method

The view (that is, the draw method) should end with the following statements:

graphics::present(ctx)?;
timer::yield_now();

In the typical double-buffering technique used by OpenGL, all the ggez drawing operations do not output graphics directly on the screen but in a hidden buffer. The present function quickly swaps the shown screen buffer with the hidden drawn buffer, with the effect of immediatelydisplaying the scene and avoiding the flicker that could appear otherwise. The last statement tells the operating system to stop using the CPU for this process until the next frame must be drawn. By doing this, if the processing of a frame is quicker than a time frame, the application avoids using 100% of the CPU cycles.

So, we have finished examining the gg_ski project. In the next section, we will examine how the gg_silent_slalom project builds on this project to create a slalom game with no sound or text.

Implementing the gg_silent_slalom project

In this section, we will examine the gg_silent_slalom project, which is an implementation of the ggez framework of the gg_silent_slalom game presented in the preceding chapter. Here, we will onlyexamine the differences between thegg_skiproject and the silent_slalomproject.

As we saw in the preceding section, ggez handles input as events. In this project, two other key events are handled—Space and R:

KeyCode::Space => {
self.input.started = true;
}
KeyCode::R => {
self.input.started = false;
}

The spacebar is used to command the start of the race, and so it setsthestartedflag totrue. TheRkey is used to reposition the ski at the beginning of the slope, and so it sets the started flag tofalse.

This flag is then used in the update method, as in the following code:

match self.mode {
Mode::Ready => {
if self.input.started {
self.mode = Mode::Running;
}
}

When in Ready mode, instead of directly checking the keyboard state, the started flag is checked. The computation of speed and acceleration takes into account the time that has actually elapsed since the preceding frame computation:

self.forward_speed = (self.forward_speed
+ ALONG_ACCELERATION * self.period_in_sec * self.direction.cos())
* DRAG_FACTOR.powf(self.period_in_sec);

To compute the new forward speed, the acceleration along the slope (ALONG_ACCELERATION) is projected on the ski direction using the cosine function (self.direction.cos()), and then the result is multiplied by the elapsed time (self.period_in_sec) to get the speed increment.

The incremented speed is then multiplied by a factor that is less than 1 to take friction into account. This factor is the DRAG_FACTOR constant for a time of 1 second. To get the decrease factor for the actual time elapsed, the exponential function must be used (powf).

To compute the new horizontal position of the ski tip, the following statement is executed:

self.ski_across_offset +=
self.forward_speed * self.period_in_sec * self.direction.sin();

This multiplies the speed (self.forward_speed) by the time elapsed (self.period_in_sec) to obtain the space increment. This increment is projected on the horizontal direction using the sine function (self.direction.sin()) to get the horizontal position variation.

A similar computation is performed to compute the movement along the slope, which is actuallythe offset of the position of the gates as the ski is alwaysdrawn at the sameYcoordinate.

To draw the poles of the gates in the draw method, first, two meshes are created by using the following statements:

let normal_pole = graphics::Mesh::new_circle(
ctx,
DrawMode::fill(),
Point2::new(0., 0.),
GATE_POLE_RADIUS,
0.05,
[0., 0., 1., 1.].into(),
)?;
let finish_pole = graphics::Mesh::new_circle(
ctx,
DrawMode::fill(),
Point2::new(0., 0.),
GATE_POLE_RADIUS,
0.05,
[0., 1., 0., 1.].into(),
)?;

Here, the meshes are created directly without using a MeshBuilder object. The new_circle method requires the context, the fill mode, the center, the radius, a tolerance, and the color as parameters. Tolerance is a trade-off between performance and graphic quality. The former mesh is used to draw all the poles, except those of the finish gate, and the latter mesh is used to draw the poles of the finish gate.

Then, these meshes are drawn to show all the poles using statements such as the following:

graphics::draw(
ctx,
pole,
(Point2::new(SCREEN_WIDTH / 2. + gate.0, gates_along_pos),),
)?;

Here, the third argument (with the DrawParam type) is specified in a simple but somewhat obscure way; it is a tuple containing just one element. This element is interpreted as the position where the mesh will be drawn, corresponding to the dest method call seen in the preceding section.

So, we have now also seen the peculiarities of the gg_silent_slalom project. In the next section, we will look at the gg_assets_slalom project, which adds sound and text to our project.

Implementing the gg_assets_slalom project

In this chapter, we will examine the gg_assets_slalom project, which is an implementation of the ggez framework of the assets_slalom game presented in the preceding chapter. Here, we will onlyexamine the differences between thegg_silent_slalomproject and the assets_slalomproject.

The main difference is found in how the assets are loaded. The assets of these projects are of two kinds—fonts and sounds. To encapsulate these assets, instead of using objects with the Asset<Font> and Asset<Sound> types, ggez uses objects with the graphics::Font and audio::Source types, respectively. These assets are loaded into the constructor of the model. For example, the constructor of the Screen object contains the following statements:

font: Font::new(ctx, "/font.ttf")?,
whoosh_sound: audio::Source::new(ctx, "/whoosh.ogg")?,

The first one loadsa file containing a TrueType fontfor thectxcontext and returns an object encapsulating this font. The second one loads (for thectxcontext) a file containing an OGG sound and returns an object encapsulating this sound. Both files must be present in theassetfolder that was specified in themainfunction using the add_resource_pathmethod, and they must be in one of the supported formats.

There is an important difference in how quicksilver and ggez load their assets. quicksilver loads them asynchronously, creating future objects whose access function must ensure that the asset is loaded. On the other hand, ggez is synchronous; when it loads the assets, it blocks the application until the assets are completely loaded. The objects created are not future objects, and so they can be used immediately.

Because it uses future objects, quicksilver is more sophisticated, but this sophistication isprobablyuseless on a desktop application because, provided your application has no more than a few megabytes of assets, loading them from local storage is quite fast, and so some blockingstatements at application startup are not going to be inconvenient. Of course, to prevent slowing down animations, the assets must be loaded only at application startup, when changing the level of a game, or when the game is ending. Once an asset is loaded, it is immediately available.

The easiest asset to use is sound. To play a sound, the following function is defined:

fn play_sound(sound: &mut audio::Source, volume: f32) {
sound.set_volume(volume);
let _ = sound.play_detached();
}

Its first argument is the sound asset and the second argument is the desired volume level. This function simply sets the volume and then plays the sound using the play_detached method. This method overlaps the new sound with any other sounds that are already playing. There is also a play method, which automatically stops playing the old sounds before starting the new one.

To play a fixed-volume sound, such as one that signals the failure to enter a gate, the following statement is used:

play_sound(&mut self.bump_sound, 1.);

In addition, to make a sound proportional to the speed, the following statement is used:

play_sound(&mut self.whoosh_sound, self.forward_speed * 0.005);

The font is quite easy to use, too:

let text = graphics::Text::new((elapsed_shown_text, self.font, 16.0));
graphics::draw(ctx, &text, (Point2::new(4.0, 4.0), graphics::BLACK))?;

The first statement creates a text shape by calling the new function. It has a tuple with three fieldsas an argument:

  • The string to print (elapsed_shown_text)
  • The scalable font object to use for this text (self.font)
  • The desired size of the generated text (16.0)

The second statement draws the created text shape on the ctx context. This statement specifies a tuple that will be converted to a DrawParam valueas a third parameter. The specified sub-arguments are the destination point (Point2::new(4.0, 4.0)) and the color to use (graphics::BLACK).

So, we have now covered the whole game. In the next section, we will look at another game, which uses mouse clicks and other kinds of assets—images.

Implementing the gg_whac project

In this section, we will examine the gg_whac project, which is an implementation in the ggez framework of the famousWhack-A-Molearcade game. First of all, let's try to play it.

After running cargo run --release in the gg_whac folder, the following window will appear, which shows a lawn:

For those of you who aren't familiar with this game, here are the rules. When you click on the Start button, the following things happen:

  1. The Start button disappears.
  2. The countdown begins at the top left from 40 seconds to 0.
  3. A nice mole pops up in a random position of the lawn.
  4. The mouse cursor becomes a barred circle.
  5. If you move your mouse cursor over the mole, it becomes a cross and a big mallet appears; this mallet can be dragged by the mouse as long as you remain over the mole.

Your window should look similar to the following:

If you click the main mouse button when the mouse cursor hovers over the mole, the mole disappears and another one appears in another position. In the meantime, a counter tells you how many moles you have managed to whack. When the countdown reaches 0, you are presented with your score.

The assets

To understand the behavior of this application, first, let's look at the content of the assets folder:

  • cry.ogg is the sound produced by the mole when it pops up out of the lawn.
  • click.ogg is the sound of the mallet when it hits the mole.
  • bump.ogg is the sound of the mallet when it hits the lawn but misses the mole.
  • two_notes.ogg is the jingle produced when the game ends because the countdown has elapsed.
  • font.ttf is the font used for all the visible text.
  • mole.png is the image of the moles.
  • mallet.pngis the image of the mallet.
  • lawn.jpg is the image used to fill the background.
  • button.png is the image used for the Start button.

We already saw, in the preceding section, how to load and use sounds and fonts. Here, there is a new kind of asset—images. Images are declared by statements such as the following:

lawn_image: graphics::Image,

They are loaded, at application initialization time, by statements such as the following:

lawn_image: graphics::Image::new(ctx, "/lawn.jpg")?

They are displayed by statements such as the following:

graphics::draw(ctx, &self.lawn_image, lawn_params)?;

Here, the lawn_paramsargument, with a type of DrawParam, can specify a position, a scale, a rotation, and even a crop.

The general structure of the application and events

Now, let's examine the structure of the source code. Like the preceding projects we have seen in this chapter, this project does the following:

  • Defines some constants
  • Defines a model with the struct Screen type
  • Implements the EventHandler trait with its required update and draw methods and its optional mouse_button_down_event and mouse_button_up_event methods
  • Defines the main function

The most important field of the model is mode, whose type is defined by the following code:

enum Mode {
Ready,
Raising,
Lowering,
}

The initial mode is Ready, where the countdown is stopped and the game is ready to start. When the game is running, there are the following states:

  • No mole appears.
  • One mole rises from the ground.
  • One mole rises and waits to be hit.
  • A blow of the mallet is about to hit the mole.
  • The mole that has been hit lowers into the ground.

Well, actually, the first state does not exists, because as soon as the game starts, a mole pops up, and also, as soon as you hit a mole, another mole pops up. The second and third states are represented by Mode::Raising. Simply put, when the mole reaches its full height, it is not raised.

The fourth and fifth states are represented by Mode::Lowering. Simply put, the mole lowers simultaneously with the mallet.

Regarding the input operations, it should be noted that for theEventHandlertrait, no key handling methods are implemented as this game does not use the keyboard. Instead, it uses the mouse, and so there is the following code:

fn mouse_button_down_event(&mut self, _ctx: &mut Context,
button: MouseButton, x: f32, y: f32) {
if button == MouseButton::Left {
self.mouse_down_at = Some(Point2::new(x, y));
}
}

fn mouse_button_up_event(&mut self, _ctx: &mut Context,
button: MouseButton, x: f32, y: f32) {
if button == MouseButton::Left {
self.mouse_up_at = Some(Point2::new(x, y));
}
}

The first method is invoked when a mouse button is pressed and the second one is invoked when a mouse button is released.

The third argument of these methods (button) is an enum indicating which button has been pressed; MouseButton::Left actually represents the main mouse button.

The fourth and fifth arguments of these methods (x and y) are the coordinates of the position of the mouse when its button has been pressed. Their unit of measurement is pixels and the origin of their coordinate system is the top-left vertex of the context, which in our case is the client area of the window.

Only the main mouse button is handled. When it is pressed, the point representing the current mouse position is stored in the mouse_down_at field of the model, and when it is released, it is stored in the mouse_up_at field of the model.

These fields are defined in the modelin the following way:

    mouse_down_at: Option<Point2>,
mouse_up_at: Option<Point2>,

Their value is initialized to None and is onlyset to aPoint2value by the preceding code; it is reset toNoneas soon as these events are processed by theupdatemethod. So, each mouse event is only processed once.

Other fields of the model

In addition to the fields that we have already described, the model has the following other fields:

start_time: Option<Duration>,
active_mole_column: usize,
active_mole_row: usize,
active_mole_position: f32,
n_hit_moles: u32,
random_generator: ThreadRng,
start_button: Button,

The start_time field is used to show the current remaining time during the game and to show the Game finishedtext when the game ends. It is initialized toNone, and then any time theStart button is pressed, the current time is stored in it.

The moles do not appear in totally random positions. The lawn is covertly organized into three rows and five columns. A mole appears in 1 of these 15 positions, chosen at random. The active_mole_column and active_mole_row fields contain the zero-based column and the row of the currently displayed mole.

The active_mole_position field contains the fraction of the appearance of the current mole. A 0 value means that the mole is totally hidden. A value of 1 means that the image of the mole (representing a part of its body) has completely appeared. The n_hit_moles field counts how many moles have been hit.

The random_generator field is a pseudo-random number generator used to generate the position of the next mole to show. Finally, start_button is a field representing the Start button. However, its type is not defined in a library. It is defined in this application, as we are going to explain.

Defining a widget

Business applications have windows full of small, interactive graphical elements, such as buttons and textboxes. These elements are usually named controls by Microsoft Windows documentation and widgets (from window objects) in Unix-like environments. Defining widgets using graphics primitives is a rather complex feat, so if you want to develop a business application, you should probablyuse a library that defines a set of widgets.

Neither the Rust standard library nor the ggez framework defines widgets. However, if you need just a few very simple widgets, you can develop them yourself, such as the button we will develop for this project. Let's see how this is implemented.

First of all, there is a definition of the Button type that can be instantiated for any button you want to add to your window:

struct Button {
base_image: Rc<graphics::Image>,
bounding_box: Rect,
drawable_text: graphics::Text,
}

Our button is just an image resized as we want with text centered on it. This image is the same for all the buttons, and so it should be shared throughout the program to save memory. This is why the base_image field is a reference-counted pointer to an image.

The bounding_box field indicates the desired position and size of the button. The image will be stretched or shrunk to fit this size. The drawable_text field is a text shape that will be drawn over the image of the button as its caption. The Button type implements several methods:

  • new to create a new button
  • contains to check whether a given point is inside the button
  • draw to display itself in the specified context

The new method has many arguments:

fn new(
ctx: &mut Context,
caption: &str,
center: Point2,
font: Font,
base_image: Rc<graphics::Image>,
) -> Self {

The caption argument is the text to display inside the button. The center argument is the desired position of the center of the button. The font and base_imagearguments are the font and image to use.

To create our button, the following expression is used:

start_button: Button::new(
ctx,
"Start",
Point2::new(600., 40.),
font,
button_image.clone(),
),

It specifies "Start" as the caption, a width of 600 pixels, and a height of 40 pixels.

To draw the button, first, we check whether the main mouse button is currently pressed using this expression:

mouse::button_pressed(ctx, MouseButton::Left)

By doing this, it is possible to make the button appear like it is being pressed to give visual feedback of the button's operation. Then, we check whether the mouse cursor is inside the button using this expression:

rect.contains(mouse::position(ctx))

This turns the color of the button caption red when the mouse hovers over the button to show the user that the button can be clicked on. So, we have now looked at the most interesting parts of this project, which ends our look into the ggez framework.

Summary

We have seen how to build two-dimensional games for the desktop using the ggez framework. This framework not only allows us to structure the application according to the animation-loop architecture and the MVC architectural pattern but also to get discrete input events. In addition, we have seen why a linear algebra library can be useful for graphical applications.

We created and looked at four apps—gg_ski, gg_silent_slalom, gg_assets_slalom, and gg_whac.

In particular, we learned how to build a graphical desktop app using theggez framework, which is structured according to the MVC architecture, and how to implement both an animation-loop architecture and an event-driven architecture, possibly in the same window. Additionally, we also learned to draw graphical elements on a web page usingggez, as well as loading and using static resources usingggez. At the end of the chapter, we encapsulated two-dimensional points and vectors in a structure and saw how to manipulate them using thenalgebralibrary.

In the next chapter, we will look at a completely different technology: parsing. Parsing text files is useful for many purposes, in particular for interpreting or compiling a source code program. We will take a look at the nom library, which makes parsing tasks easier.

Questions

  1. What is the difference between a vector and a point in linear algebra?
  2. What are the geometrical concepts corresponding to algebraic vectors and points?
  3. Why can capturing events be useful, even in an animation loop-oriented application?
  4. Why can loading synchronous assets be a good idea in a desktop game?
  5. How does ggez get input from the keyboard and mouse?
  6. What are the meshes used in the ggez framework?
  1. How can you build a ggez mesh?
  2. How do you obtain a desired animation frame rate using ggez?
  3. How do you draw a mesh in the desired position using ggez, with the desired scale and rotation values?
  4. How do you play sound using ggez?

Further reading

The ggez project can be downloaded from https://github.com/ggez/ggez. This repository contains many example projects, including a complete asteroid arcade game.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.239.123