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.

Thursday, February 18, 2010

Eating the eggs

In this chapter, I will not talk about how to cook or fry the egg. Instead I will teach you how to displays the egg on the screen and how to make the game able to recognize when it has been eaten by the snake.

We will create new class that takes care of creation and presentation of the egg, add new code that checks collision between the egg and the snake and increases the length of the snake, after the egg is eaten. At the end we will make some improvements in the code that controls the movement of the snake.

Egg class

Class represents the object in the game that can be eaten by the snake. Implementation of the class takes care of creation, positioning and drawing. We will put complete implementation of the class into one file (Egg.as) and put this file inside Classes folder. Here is the content of the file:

package Classes
{
  import flash.geom.*;
  import flash.display.*;

  public class Egg
  {
    private var position:Point = null;
    private var eaten:Boolean = false;
    private var phase:int = 0;

    private static var positions:Array = new Array(new Point(120,430),new Point(300,320),new Point(70,30),new Point(240,280),new Point(400,130),new Point(360,180));
    private static var nextPosition:int = 0;

    public function Egg(initial:Boolean)
    {
      position = positions[nextPosition];

      phase = initial ? 10 : 1;

      nextPosition++;

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

    public function getPosition():Point
    {
      return position;
    }

    public function active():Boolean
    {
      return (phase == 10);
    }

    public function eat():void
    {
      if (active())
        eaten = true;
    }

    public function update():Boolean
    {
      if (eaten)
      {
        phase--;

        if (phase <= 0)
          return false;
      }
      else
      {
        phase++;

        if (phase > 10)
          phase = 10;
      }

      return true;
    }

    [Embed(source="../Images/Egg.png")] private var eggImg:Class;

    private var bitmap:BitmapData = null;

    public function draw(screenBuffer:BitmapData,visible:Rectangle):void
    {
      if (bitmap == null)
      {
        bitmap = new BitmapData(300,30,true,0x00000000);
        bitmap.draw(new eggImg());
      }

      screenBuffer.copyPixels(bitmap,
                              new Rectangle(300 - phase * 30,0,30,30),
                              new Point(position.x - 15,visible.y - position.y - 15));
   }
  }
}

Implementation of this class is simple compared to the Snake class, but it still requires some explanation. Class has the following members:
- position: contains horizontal and vertical position of the egg in the game world
- eaten: is set to true when the snake eats the egg
- phase: is used to display the egg image with different levels of transparency
- positions: contain six different positions, where eggs are displayed. You can add new positions if you like
- nextPosition: index of the Point in positions Array that will be used to set the position of new egg
- constructor (Egg): parameter initial must be set to true for the egg that is made at the beginning of the stage. First egg is visible after it has been made (phase set to 10), other eggs are not (phase set to 1). Position of the first egg is stored in the first element in positions Array, position of the second egg is in the second element... when the value of nextPosition variable reaches the end of the Array, it is set to zero and first element is used again.
- getPosition: returns position of the egg.
- active: returns true only when phase variable has value 10.
- eat: this function will be called when the snake is inside the range of the egg. Function checks if egg is active and it has not been eaten before and then sets the eaten to true.
- update: function will be called once per frame. When the egg is eaten, value of phase will be decrease by one until it reaches the zero. At this point function returns false, which indicates that egg has been eaten and it has completely disappeared and can be replaced with new one.
- draw: creates BitmpaData from embedded image if necessary and displays the egg image with required level of transparency. Level of transparency is controlled with phase variable.

Egg.png contains egg icons with ten different levels of transparency and can be found inside Images folder in project package for this chapter.


Game.as

Egg class implements all the functionality to display the egg on the screen, we just have to use it. Open Game.as file and add definition of egg variable right after snake variable:

private var egg:Egg = null;

We only need one variable, because there can be only one egg in the game at the time. First egg object will be made at the beginning of the game (New_Game function), value of initial parameter is set to true.

egg = new Egg(true);

Now we can display the egg by calling draw function int Draw_Game function. Insert this code before the code that draws the snake:

if (egg != null)
  egg.draw(screenBuffer,new Rectangle(0,SCREEN_HEIGHT,SCREEN_WIDTH,SCREEN_HEIGHT));

We have to call update function once per frame. Draw_Game function is executed once per frame, that's why we will call update here. We have to call it only when GAME_PLAYED state is active. Replace existing code in Draw_Game function for this state with:

if (egg != null)
  if (!egg.update())
    egg = new Egg(false);

if (snake != null)
{
  snake.move();

  CheckCollision();
}

Function update returns false when the egg has been eaten and it has completely disappeared. This is good time to create new egg, but this time with initial parameter set to false.

Right after snake.move statement, function CheckCollision checks if there is collision between the egg and the snake. Implementation of this function is our next task.

CheckCollision

Function CheckCollision checks if there is collision between the egg and the head of the snake. The head is inside the range when horizontal or vertical distance is less or equal 12 pixels. This is not very precise solution but in practice it works OK:

private function CheckCollision():void
{
  var snakePosition:Point = snake.getPosition();
  var snakeDirection:int = snake.getDirection();

  if ((egg != null) && egg.active())
  {
    var distanceToEgg:Point = egg.getPosition().subtract(snakePosition);

    if ((Math.abs(distanceToEgg.x) <= 12) && (Math.abs(distanceToEgg.y) <= 12))
    {
      egg.eat();
      snake.append();
    }
  }
}

When the snake is close enough to the egg, eat function is called which sets the eaten variable to true. Length of the snake is increased within append function from Snake class.

Snake.append

Task of append function is quite simple: it adds new part at the end of the snake body. In fact it copies all three properties (position, direction and phase) of the last part, adjusts the position and phase and pushes new values at the end of Array.

public function append():void
{
  var position:Point = partPosition[partPosition.length - 1];
  var direction:int = partDirection[partPosition.length - 1];
   var phase:int = partPhase[partPosition.length - 1];

  switch (direction)
  {
  case NORTH:
    partPosition.push(new Point(position.x,position.y - SPEED));
    partPhase.push((phase + 1) % 9);
    break;
  case EAST:
    partPosition.push(new Point(position.x - SPEED,position.y));
    partPhase.push(((phase - 1) < 0 ? 8 : phase - 1) % 9);
    break;
  case SOUTH:
    partPosition.push(new Point(position.x,position.y + SPEED));
    partPhase.push(((phase - 1) < 0 ? 8 : phase - 1) % 9);
    break;
  case WEST:
    partPosition.push(new Point(position.x + SPEED,position.y));
    partPhase.push((phase + 1) % 9);
    break;
  }

  partDirection.push(direction);
}

You can now compile and play the game. When the game begins there is an egg located in upper-left corner. When the snake is close enough, the egg disappears and new one appears in lower right corner. Length of the snake is slightly increased, maybe you will not notice this after first egg, but after eating five or six eggs, snake will become significantly longer.

I am sure, you had problems controlling the snake. It is not your fault, controls are not very precise and too complicated. We have to fix this.

Phase 4

In chapter 16 we implemented advanced version of snake. If you remember, snake can change direction only when phase value of leading part equals 0. In the worst case scenario, snake moves by 32 pixels (SPEED * 8) before it changes the direction. This is the reason why it is sometimes almost impossible to hit the egg. I made some changes in source code of move function: now snake can change direction even if phase value of leading part equals 4.

New version of move function can be found in Snake.as file inside project package for this chapter. If you recompile and try the game again, you will see that snake now changes direction almost immediately after mouse is pressed.

SetGameCursor/MouseDown_Game

At the moment, controlling of the snake requires a lot of mouse movement. We can make things a lot more simple if we change the rules. When snake is moving in vertical direction, we will only check if mouse cursor is on the left or right side at the time of mouse click. When snake is moving in horizontal direction, we will check if mouse is above or below. This makes the code much more simple and controlling much easier.

New version of SetGameCursor function in Game.as now looks like this:

private function SetGameCursor():void
{
  var mx:int = mouseX;
  var my:int = mouseY;

  my = SCREEN_HEIGHT - my;
  var position:Point = snake.getPosition();
  var direction:int = snake.getDirection();
  var cursorDirection:int = 0;

  if ((direction == Snake.NORTH) || (direction == Snake.SOUTH))
    if (mx - position.x > 0)
      cursorDirection = Snake.EAST;
    else
      cursorDirection = Snake.WEST;
  else
    if (my - position.y > 0)
      cursorDirection = Snake.NORTH;
    else
      cursorDirection = Snake.SOUTH;

  SetMouseCursor(cursorDirection,false);
}

Source code for GAME_PLAYED state inside MouseDown_Game function:

case GAME_PLAYED:
  {
    my = SCREEN_HEIGHT - my;
    var position:Point = snake.getPosition();
    var direction:int = snake.getDirection();

    if ((direction == Snake.NORTH) || (direction == Snake.SOUTH))
      if (mx - position.x > 0)
        snake.setDirection(Snake.EAST);
      else
        snake.setDirection(Snake.WEST);
    else
      if (my - position.y > 0)
        snake.setDirection(Snake.NORTH);
      else
        snake.setDirection(Snake.SOUTH);
}

Controlling is now much more comfortable, source code is much shorter and more clear.


No comments:

Post a Comment