I never thought development of web games can be that simple until I started development of my first game with Adobe Flex. I made two games, without any prior experience with Action script, in only ten months: "Fitter" and "RGB".

I found many answers to my questions on different forums and web sites, and it is time to share my knowledge with Flex community.

Wednesday, February 24, 2010

Collision detection

This is not the first time that we are implementing collision detection in Snakes Adventure. First time we've made it in the game menu, where collision between mouse cursor and game options is detected on mouse events. In previous chapter we implemented CheckCollision function, which once per frame, detects collision between the head of the snake and the egg. In this chapter we will improve collision detection between the head of the snake and the egg and add new code that checks if the head collides with the edges of the frame or other body parts.

getBounds

We will implement getBounds function in both classes: Snake and Egg. Both functions will return position and size of the sensitive area of the object. Position and size will be stored inside Rectangle object.

Function getBounds for Egg class (Egg.as) is simple:

public function getBounds():Rectangle
{
  return new Rectangle(position.x - 5, position.y + 9, 11, 16);
}

Function returns the area around the centre of the egg, which is a lot smaller than area, where pixels from the source image are copied to (because most of those pixels are transparent).

Function getBounds for Snake class (Snake .as) is far more complex, because it returns the area of one part of the body, not the whole body. Function requires parameter part - the index of the part.

public function getBounds(part:int):Rectangle
{
  var pos:Point = partPosition[part];
  var direction:int = partDirection[part];
  var phase:int = partPhase[part];
  var rectangle:Rectangle = null;

  if (part == 0) //head
  {
    phase = partPhase[1];

    switch (direction)
    {
    case NORTH:
      rectangle = new Rectangle(pos.x - 6 + northHeadOffset[phase],pos.y + 12,13,16);
      break;
    case EAST:
      rectangle = new Rectangle(pos.x - 4,pos.y + 6 - eastHeadOffset[phase],16,13);
      break;
    case SOUTH:
      rectangle = new Rectangle(pos.x - 6 + southHeadOffset[phase],pos.y + 4,13,16);
      break;
    case WEST:
      rectangle = new Rectangle(pos.x - 12,pos.y + 6 - westHeadOffset[phase],16,13);
      break;
    }
  }
  else
  if (part == partPosition.length - 1) //tail
    switch (direction)
    {
    case NORTH:
      rectangle = new Rectangle(pos.x - 5 + northHeadOffset[phase],pos.y + 3,11,10);
      break;
    case EAST:
      rectangle = new Rectangle(pos.x - 6,pos.y + 5 - eastHeadOffset[phase],10,11);
      break;
    case SOUTH:
      rectangle = new Rectangle(pos.x - 5 + southHeadOffset[phase],pos.y + 7,11,10);
      break;
    case WEST:
      rectangle = new Rectangle(pos.x - 5,pos.y + 5 - westHeadOffset[phase],10,11);
      break;
    }
  else //body
    switch (direction)
    {
    case NORTH:
      rectangle = new Rectangle(pos.x - 6 + northHeadOffset[phase],pos.y + 2,12,4);
      break;
    case EAST:
      rectangle = new Rectangle(pos.x - 2,pos.y + 6 - eastHeadOffset[phase],4,12);
      break;
    case SOUTH:
      rectangle = new Rectangle(pos.x - 6 + southHeadOffset[phase],pos.y + 2,12,4);
      break;
    case WEST:
      rectangle = new Rectangle(pos.x - 2,pos.y + 6 - westHeadOffset[phase],4,12);
      break;
    }

  return rectangle;
}

Function returns rectangular area of each body part, position and size are similar to those, used in the draw function, but area is a lot smaller.

CheckCollision

Current version of collision detection between the head of the snake and the egg work like this: if horizontal or vertical distance between both positions was less than 12 pixels, collision was detected.

Now that we have both getBounds functions implemented, we can use them to improve collision detection between the head of the snake and the egg. Replace content of CheckCollision function in Game.as:

var headBounds:Rectangle = snake.getBounds(0);

//check egg
if ((egg != null) && egg.active())
  if (headBounds.intersects(egg.getBounds()))
  {
    egg.eat();
    snake.append();
  }

First, area of snakes head is stored into headBounds Rectangle and if egg is "active", intersects function checks if there is collision between two Rectangles. The rest of the source code is the same as in previous version.

Collision detection is more precise now, because snake's head movement and direction are now taken into account. But you will most probably not notice any difference while playing the game.

In the original game, the snake dies when the head hits the edge of the game area or body part. This is not hard to implement. Add this section at the end of CheckCollision function:

//check the edge of the screen
if ((headBounds.x <= 0) || (SCREEN_WIDTH - headBounds.x - headBounds.width <= 0) ||
    (headBounds.y - headBounds.height <= 0) || (SCREEN_HEIGHT - headBounds.y <= 0))
{
  snake.kill();
  return false;
}

Simple, isn't it? The if statement checks if headBounds Rectangle is outside left, right, bottom or top edge of the game frame. If it is, snake dies. We are using unimplemented function kill from Snake class, that will be implemented at the end of the chapter. Statement return false; is currently illegal, because function declaration says that function doesn't return anything. Replace void with Boolean in the function declaration. From now on, we will use return value to indicate if snake died (false) or not (true).

The following section of code checks if the head of the snake hit the body part. Add it to the end of CheckCollision function:

//check body
for (var i:int = 7; i < snake.getLength(); i++)
{
  var partBounds:Rectangle = snake.getBounds(i);

  if (headBounds.intersects(partBounds))
  {
    snake.kill();
    return false;
  }
}

return true;

This code checks if rectangular area of body parts intersects with the head. If collision is detected, snake dies, function exits and value false is returned. If there is no collision, function exits and returns true. There is another undefined function from Snake class used here - getLength function will be implemented at the end of the chapter.

Great, now the game knows when the player made mistake. There are only few things we have to do, before we are done:
- stop the game
- display animation of dying snake
- perform fade out
- restart the level

GAME_KILLED/FADE_OUT

These two constants define two new game states. During the GAME_KILLED state, animation of dying snake will be displayed. GAME_FADE_OUT state will be active for one second and within this second, fade out effect will be applied. Add both definitions to other constants at the top of Game.as:

private static const GAME_KILLED:int = 5;
private static const GAME_FADE_OUT:int = 6;

Now we will use the value that is returned by CheckCollision function. Change the CheckCollision statement in Draw_Game function (GAME_PLAYED state) to:

if (!CheckCollision())
  ChangeState_Game(GAME_KILLED);

When the false value is returned (snake died), game state is change to GAME_KILLED.

Add the source code for both states inside switch statement in Draw_Game function:

case GAME_KILLED:
  if (!snake.kill())
    ChangeState_Game(GAME_FADE_OUT);
  break;
case GAME_FADE_OUT:
  Apply_FadeEffect(gameTime);

  if (gameTime >= 1)
    Restart_Game();
  break;

While the GAME_KILLED is active kill function (still unimplemented function that takes care of animation of dying snake) from Snake class is called until it returns value false. At this point GAME_FADE_OUT becomes active, during this state fade out effect is applied for one second and when the screen is completely black, Restart_Game function is called.

Before we implement this function, you can make a little change in SetMouseCursor function. Change the last statement of the function to:

SetMouseCursor(cursorDirection,gameState > GAME_PLAYED);

Now the grey mouse cursor icon will be displayed while GAME_KILLED or GAME_FADE_OUT state is active.

Restart_Game

Implementation of this function is very similar to New_Game function, that is why the code is positioned next to it:

private function Restart_Game():void
{
  snake = new Snake(SCREEN_WIDTH / 2,SCREEN_HEIGHT / 3,20);
  egg = new Egg(true);

  ChangeState_Game(GAME_FADE_IN);
}

New Snake and Egg objects are created the same way as they are in New_Game function, game state is also set to GAME_FADE_IN. The only difference is that New_Game function also sets level variable to 1.

There is one more thing we have to do, to make sure everything looks the same after restart. We have to reset nextPosition counter in Egg class, otherwise the initial egg will most probably not be at the same position. Replace Egg constructor in Egg.as file:

public function Egg(initial:Boolean)
{
  if (initial)
  {
    phase = 10;
    nextPosition = 0;
  }
  else
    phase = 1;

  position = positions[nextPosition];

  nextPosition++;

  if (nextPosition == positions.length)
    nextPosition = 0;
}

If value of parameter initial is true, value of nextPosition variable is set to 0. The rest of the code is the same as before.

Snake class

We have to implement all the functions in Snake class (Snake.as) that we used in CheckCollision function: getLength and kill. Implementation of the first one is simple:

public function getLength():int
{
  return partPosition.length;
}

Function returns the number of body parts, which is equal to number of objects in each Array from Snake class.

Before we start implementing the animation of dying snake, I advise you to try the game at the bottom of the page to see how animation snake looks like. In the first phase body parts are slowly transformed into bones. In the second phase bones are transformed into half-transparent bones and in the final phase bones slowly disappear.

Of course, this animation requires new graphics. I added new images into Snake.png file, that now looks like this:


If you download project package for this chapter, you can find this image inside Images folder. Body parts, bones and half-transparent bones are separated by 80 pixels in vertical direction.

Snake.kill

The kill function is called once from CheckCollision function and once per frame from Draw_Game while GAME_KILLED state is active. When kill function returns false, animation of dying snake is complete and game state GAME_FADE_OUT becomes active.

Before we start implementing kill function, we will add two variables at the beginning of class:

private var killed:Boolean = false;

The first time kill function will execute, the value of variable will be set to true.

private var deadPart:Array = new Array();

This array will be used for animation, each body part will have its own numeric value inside this array, that will represent the animation phase.

Add implementation of kill to Snake class:

public function kill():Boolean
{
  if (!killed)
  {
    killed = true;

    for (var i:int = 0; i < partPosition.length; i++)
      deadPart.push(0);
  }

  if ((deadPart.indexOf(0) == -1) &&
      (deadPart.indexOf(1) == -1) &&
      (deadPart.indexOf(2) == -1))
    return false;

  for (var j:int = 0; j <= 2; j++)
    if (deadPart.indexOf(j) != -1)
      while (true)
      {
        var index:int = (int)(Math.random() * (deadPart.length - 0.1));

        if (deadPart[index] == j)
        {
          deadPart[index] = deadPart[index] + 1;
          return true;
        }
      }

  return true;
}

The first time kill function is executed, killed variable is set to true and deadParts Array gains one numeric object with zero value for each body part.

"if ((deadPart.indexOf(0) ..." statement checks if there is an object in deadParts Array with value 0, 1 or 2. Function indexOf checks if there is an object in the array, that has value equal to value of the parameter, and returns its index. -1 indicates that there is no such object.

If all objects have value 3, it means that animation is complete and function exits and value false is returned.

The last section of the code checks if there is an object in deadParts Array with value 0, 1 or 2. If there is no object with value 0, first phase of animation is complete. If there is object with value 1 in the Array in means that animation is still in the second phase.

Code inside while loop randomly selects an object in the array and if this object has desired value (value of j counter), its value is increased by one. For example, if in the first phase of animation, fifth object in the array is randomly selected, its value is increased from zero to one and player can see the fifth body part transformed into bones.

Snake.draw

Animation of dying snake requires a lot of changes in draw function, unfortunately, source code of this function has become much too long to be displayed on this page. I advise you to download project package for this chapter and copy the content of draw function from Snake.as into your own project.

New Snake.png file requires changes in the creation of BitmapData object constructor, because the height of the source image has increased:

bitmap = new BitmapData(96,240,true,0x00000000);

At the beginning of for loop that iterates through all body parts, there is new definition of deadPartOffset variable with value set to 0. When deadPart array is not empty, game is in the "dying snake animation" mode. If deadPart object of the part, that function is currently drawing, has value 3, drawing is skipped. Otherwise deadPartOffset value is calculated from deadPart object value multiplied by 80, which is the vertical distance between source images for each phase of animation:

var deadPartOffset:int = 0;

if (deadPart.length > 0)
  if (deadPart[i] == 3)
    continue;
  else
    deadPartOffset = deadPart[i] * 80;

Value of deadPartOffset variable has been used in each copyPixels statement. Here is one example:

screenBuffer.copyPixels(bitmap,
                        new Rectangle(72,51 + deadPartOffset,10,11),
                        new Point(pos.x - 12,visible.y - pos.y - 5));


No comments:

Post a Comment