Christopher Bennage

not all who wander are lost

Building a Game With JavaScript: Making Things Move

This is a continuation from the previous post.

Setting The Stage

The game we’re building will have waves of enemy ships fly in to attack the player’s units. Let’s begin by making a simple enemy as well as some dummy targets for them to attack. I’m going to keep the graphics very simple for the moment. Likewise we are going to focus on the enemy behavior and not worry about any player interaction just yet.

Here’s a demo of what we’ll make. Click on the start screen to transition into the game. The little yellow rectangles are our enemy ships. Each one projects its own target as a little red circle. Once it touches its target, it projects a new one and then flies toward it.

Let’s start from the top down. Our enemy units will “live” in our main screen for the game. (At least for the time being.) This screen needs to expose the same interface that we had for the start screen we made in the last post. We’ll also add a start method that we’ll call just once in order to initialize things.

Implementation

Here’s the implementation:

Explanation

The entities array will contain a list of the enemies we’re tracking. I used the name “entity” because this is a common term in game development. In general, it means something that has behavior and is drawn to the screen. Thus, you can expect entities to have update and draw methods. This is not a hard and fast definition though. You’ll find that the specifics of the definition can vary among engines, frameworks, and developers.

In our start function we populate entities by invoking our (as yet undefined) makeEnemyShip function. I’m passing in two numbers that makeEnemyShip will use to set the x and y position of the ship. I could have used random numbers or even hard coded values, however deriving from the loop’s controls makes it easy to cluster all the ships in the upper left corner of the screen.

The draw and update functions for the screen are very similar. They both iterate over entities and invoke the corresponding function on each entity. They also pass along the necessary context. For draw, this is the 2D drawing context of the canvas and for update it’s the elapsed time since the last frame.

Notice how the loop is structured differently from the loop in start. This is a performance optimization; though it has little consequence with so small an array. On some browsers, the call to length is a bit expensive. (Especially in cases when the array isn’t an array, but something that is array-like.) This adds up when you make the call once per iteration of the loop. We move it out of the loop so that we only call it once. Check out this test. Performance optimizations are tricky and change every time new browsers are released. It’s easy to get confused, and I recommend profiling your code frequently to look for hot spots rather than just guessing about optimizations. I hope to talk more about them later, but if you want more now check out the book High Performance JavaScript by Nicholas C. Zakas.

I had originally written my loops using the newer Array.forEach to iterate over entities. However, this proved to be significantly slower than a for loop.

The screen’s draw method also resets the canvas at the beginning of each iteration. If we did not do this, then every thing we drew on previous frames would still be present. For the start screen, I used clearRect however here I used fillRect with a solid color.

Here’s a function that will produce a simple enemy. It follows the same structure we’ve been using, update to handle the behavior and draw to actually draw it on the screen.

Some Bad Guys

Our enemy ships are a little more complicated than the screen they live on. Visually, they appear to have two components. The little yellow rectangle that moves about the screen and the phantom target that they project as a little red circle. In the final game, they will target one of the player’s units. However, the logic is very similar. In fact, it may become useful in debugging to how each enemy ship render something over it’s actual target.

Implementation

Explanation

Each enemy ship will be responsible for tracking its own state. In this code, the state is captured in a closure. In later code, we’ll track the track in a more traditional way. (I haven’t ran tests yet but I think that using a closure may have a performance impact.)

All of these variables represent the enemy ship’s state.

var position = { x: 0, y: 0 };
var orientation = 0;
var turnSpeed = fullCircle / 50;
var speed = 2;
var target = findNewTarget();  

position is the location on the screen where we will render our ship.

Technically, the is the position in “world space”. World space is the logical space that entities in your game “live in”. This is distinct from “screen space”, which corresponds to the actual pixels on the screens. You can think of it this way: in your game you might have a circle with a radius of 10 and located at (100,100). However, where you draw it on the screen will depend upon where the player is viewing it from. If the player zooms in, the circle will grow larger but this doesn’t change the logical position or radius of the circle. We use the term “projection” to describe this. We project from world space into screen space based upon how the player is viewing the game world. The simplest project of course is just 1:1. Which means that there is no difference between world space and screen space. That’s what will stick with for the moment.

orientation is the direction the ship is currently facing. Our ship will always travel in the direction of its orientation. This constraints causes the ship travel in smooth arcs as opposed to abruptly changing its course.

turnSpeed and speed represent how quickly the ship can turn and how fast it can travel respectively. We won’t be modifying these values after setting them, which means the ship will turn and travel at constant rates. These values represent the rates of change for orientation and position. Note also, we defined turnSpeed in terms of fullCircle. This is an alias for Math.PI * 2; we are dealing in radians and not degrees.

target is a value with the shape { x: Number, y: Number }. The ship will always attempt to move towards this value by adjusting its orientation.

Update

The update function is the real meat of the enemy ship. First, we check to see if we are close to our target. If we are close enough, we consider our goal accomplished and we set a new target. Otherwise, we change our orientation so that we are flying toward our current target.

var y = target.y - position.y;
var x = target.x - position.x;
var d2 = Math.pow(x, 2) + Math.pow(y, 2);

Here, x and y are really the distance between target and position along the respective axises. We want to know these values in order to calculate the distance between them. In general, you use the Pythagorean theorum to calculate distance. For deeper dive into the math, watch Distance Formula on Khan Academy. Finding the actual real distance uses a square root and calculating a square root is an expensive operation that’s best to avoid whenever you can.

We can bypass the need by working with the distance² (hence the variable name d2). We compare this against the hard-coded value of 16 (that’s 4²). In other words, if the distance between the ship and its target is less than 4 units we find a new target.

if (d2 < 16) {
    target = findNewTarget();
}

Once we’ve established what the ship’s target should be, we want the ship to move toward the target. As I’ve just mentioned, I’ve chosen to have the ship move at a constant speed. This means that it does not slow down or speed up. The only thing it can do is to change the direction it’s facing (orientation). These sort of constraints determine the personality and character of your game. Bear in mind, you don’t need to simulate the physics to have a fun game. Instead, you need to establish behaviors for your game entities that are merely fun. Fortunately, fun behaviors can often be easier to implement that the actual physics. I recommend taking a look at the demo and tweaking the turnSpeed and speed values to get a small taste for how the behavior can affect the game’s character.

In order to change the ship’s orientation we need to first determine where the ship ought to be facing. The values of x and y we just calculated can be interpreted as a vector. Meaning, it represents the direction and distance (magnitude) from the ship to the current target. To extract the actual angle (in radian) we use Math.atan2(x,y).

var angle = Math.atan2(y, x);
var delta = angle - orientation;

So now we have the direction the ship wants to face, angle, and the direction that it is facing, orientation. We calculate the difference between them and store it as delta.

The basic idea is that add the value of turnSpeed to orientation once each invocation of udpate until delta is 0 (meaning that the ship is flying directly at the target). However, some values of delta will cause the ship to “turn the wrong way”. For example, imagine that the ship is facing the top of the screen and that we’ve defined that as orientation === 0. Now, imagine that the target is directly to its right. The value of angle would be π/2 (or 90°). Adding turnSpeed to orientation each frame increments the value from 0 to π/2. However, if the target is directly to the left, then the value of angle would be 3π/2 (or 270°). Simply incrementing orientation would cause the ship to turn right and keep turning right. This is unintuitive behavior; we expect the ship to turn the shorted distance. In order to accomplish this, we translate any value of delta higher than π (180°) by subtracting fullCircle. This normalizes the value of delta between -π and π (or between -180° and 180°).

var delta_abs = Math.abs(delta);
if (delta_abs > Math.PI) {
    delta = delta_abs - fullCircle;
}

We take the absolute value of delta because otherwise we’d have to check for delta < -Math.PI as well. Also, we’ll use delta_abs again.

If delta is 0, we don’t need to change orientation. When it is different we need to modify the value of orientation.

if (delta !== 0) {
    var direction = delta / delta_abs;
    orientation += (direction * Math.min(turnSpeed, delta_abs));
    orientation %= fullCircle;
}

First, we decide how much to change it using Math.min(turnSpeed, delta_abs). We could just use turnSpeed. However if we did it’s likely that delta would never quite be 0 and (depending on the size of turnSpeed) it could result is jittery motion. Secondly, we want to decided which direction to turn: positive values to the right and negative values to the left. We multiply the amount direction to change the sign, because direction will only ever be 1 or -1. Finally, we modulo orientation to ensure that it stays within a range of -2π to 2π. Otherwise, the calculation of delta would be off.

Our last step in update is to modifiy the actual position using the latest orientation and speed.

position.x += Math.cos(orientation) * speed;
position.y += Math.sin(orientation) * speed;

Some basic trigonometry is fairly fundamental for most game development. If you’re mathematically lost at this point, I recommend reviewing over at Khan Academey.

Here’s the geometric interpretation of the code. We want the ship to move a distance of speed in the direction of orientation. To do this, we need to project this vector (distance and direction) on the x and y axises. Since the distance is the length of the hypothenuse of right triangle, cosine gives us the x part and sine gives us the y part. We can then add these values to the current position.

Draw

Drawing the ship to the screen is a bit easier to follow. Here’s the flow of the logic:

  • Save the state of the drawing context.
  • Transform the context to make it easier to draw our ship.
  • Draw the ship.
  • Restore the context back to its original state.
  • Draw the target

      function draw(ctx) {
    
          ctx.save();
    
          ctx.translate(position.x, position.y);
          ctx.rotate(orientation);
    
          ctx.fillStyle = 'yellow';
          ctx.fillRect(-3, -1, 6, 2);
    
          ctx.restore();
    
          ctx.beginPath();
          ctx.fillStyle = 'rgba(255,0,0,0.5)';
          ctx.arc(target.x, target.y, 2, 0, Math.PI * 2, true);
          ctx.fill();
      }
    

Recall that ctx is the drawing context for the canvas. The context provide a useful API that allows us to move it around before we draw on it. This is analgous to having a sheet of paper that you might shift and rotate in order to make it easier to draw something complicated. This is the purpose of the translate and rotate methods.

First, we use ‘save’ to establishing a checkpoint for the drawing context that we can easily revert back to using ‘restore.’ The calls to translate and rotate modify the state of the drawing context. This modified state is very specific to the drawing of our enemy ship. If we didn’t translate and rotate the canvas, we’d have to do a lot more work to figure out where to draw the four corners of the rectangle.

I decided that I want my ship to be 6px long and 2px wide. Since I want the middle of the middle of my ship to be the point where it rotates, I shift by half the length and half the width. Hence, the values passed to ctx.fillRect(-3, -1, 6, 2). This point the new origin (0,0) at the middle of the rectangle, and it makes our call to rotate behave intuitively. If we used ctx.fillRect(0, 0, 6, 2) instead, then the ship would appear to rotate around its upper left corner. We’ll use these same techniques once we switch to using sprites.

After we restore the context’s state, we draw the target. We don’t bother using rotate because it’s meaningless to rotate a simple circle. Likewise, we don’t bother translate since the drawing logic is so simple.

The canvas is a broad topic in itself. I recommend taking a look at the tutorials over at MDN. A handy reference for the APIs themselves can be found on MSDN.

Comments