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.

Saturday, February 6, 2010

Snake - simple version

In this chapter we will work on the main character of the game - the Snake. We will create the simple version of the snake, that looks and acts similar to the snake in the original game. In the next chapter we are going to improve the appearance of the snake, everything else will remain the same.

Before you continue reading, it is best that you start the game and see for yourself, what will be implemented throught this chapter. Control snake movement with mouse control: direction of movement is changed when mouse button is pressed, vector between mouse cursor and snake's head defines new direction. If snake runs out of the screen, just reload the page.




Snake class

ActionScript is very powerful object oriented programming language and to reach its full potential, you have to use custom classes whenever possible. Maybe it would be better if we used custom classes earlier in this tutorial, for example: for implementation of state automaton, we could create custom classes for all states. Anyway, it is time to create our first custom class named Snake. All functionality, that concerns snake, will be implemented in this class and exposed via public methods.

Before we create Snake class we need a package that will contain this class (and other classes for this project). This is very simple, just create new folder inside project root folder and name it Classes.

Before we can use class from this package, we have to import it. Add this statement to other import statement at the top of SnakesAdventure.as file:

import Classes.*;

Create new file inside Classes folder, name it Snake.as and copy the following section into this file:

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

  public class Snake
  {

  }
}

Package instruction in the first line defines to what package all classes inside the block belong to. Two import statements define external packages that will be used in this source file. Finally, Snake class is defined.

Implementation of Snake class will be quite short, but will require a lot more explanation. We will implement class functionality step by step, all the code in the following examples belongs inside Snake class block.

There is no rule how the implementation of custom class should look like, but developers usually put public constants, enumerations and variables at the beginning of class. Put these constants into Snake class:

public static const SPEED:int = 8;
public static const NORTH:int = 1;
public static const EAST:int = 2;
public static const SOUTH:int = 3;
public static const WEST:int = 4;

SPEED constant defines the speed of snake's movement in pixels (per frame). Constants NORTH, EAST, SOUTH and WEST will be used in conjunction with direction of snake's movement.

We need two variables to describe snake's movement: position and direction; speed of the movement is constant (8 pixels). Snake body consist from many particles, current position of each particle will be kept inside its own Point object, all Point object will be stored inside Array.

private var direction:int = NORTH;
private var partPosition:Array = new Array();

When Snake object is instantiated, direction variable is set to NORTH, at the beginning of each stage snake starts to move in northern direction. Definition of partPosition Array does not define particle positions, they will be set in constructor. Both variables are private, and can be changed only from inside this class.

Constructor is class function, that is executed during creation of the object. Snake class constructor requires three parameters:

- xPos: horizontal position of first particle (head)
- yPos: vertical position of first particle (head)
- length: number of particles that represent snakes body

public function Snake(xPos:int,yPos:int,length:int)
{
  for (var i:int = 0; i < length; i++)
    partPosition.push(new Point(xPos,yPos - i * SPEED));
}

Constructor creates as many new Point objects as length parameter requires, push method adds this newly created object to the end of Array. First object (index 0) will represent the head.

Horizontal position of each particle equals value of xPos parameter, vertical position of head equals value of yPos parameter. Vertical position of each following particle is decreased by eight (SPEED). When the game starts, particles of snake body are on vertical line, particles are divided by eight pixels.

There is one thing about vertical positions of particles, that might confuse you. Vertical position of particles are decreasing from head to tail, because we are using different coordinate system that we used for drawing into screen buffer. Position (0,0) in screen buffer represents top-left corner, position (479,479) represent lower-right corner. We are using vertically inverted coordinate system for game mechanic: position (0,0) is lower-left corner and position (479,479) is top-right corner. Currently, there isn't any benefit from this approach, in fact, it will require some additional work in drawing routines, but will make life much easier when we start implementing scrolling.

Snake object

Now, that we have Snake class defined, we can create Snake object. Add declaration into Game.as:

private var snake:Snake = null;

Object will be instantiated in New_Game function:

snake = new Snake(SCREEN_WIDTH / 2,SCREEN_HEIGHT / 3,10);

New_Game function creates Snake object, that represents snake that has ten particles and its head is positioned slightly below the centre of the screen. If you compile and run the game at this point, you will not see the snake. Of course, we have to first implement functionality, that actually displays particles in screen buffer.

Drawing particles

Before we start with implementation of drawing routine, add the call of Snake.draw method in Draw_Game function, place it right after the code that colors screen buffer in green (screenBuffer.fillRect...):

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

Function draw from Snake class requires two parameters:
- BitmapData object, where the drawing will occur
- rectangle that represents visible area of the game. This functionality will be implemented in following chapters.

Add implementation of draw function into Snake.as file:

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

private var bitmap:BitmapData = null;

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

  for (var i:int; i < partPosition.length; i++)
  {
    var pos:Point = partPosition[i];

    screenBuffer.copyPixels(bitmap,
                            new Rectangle((i % 2) * 12,0,12,12),
                            new Point(pos.x - 6,visible.y - pos.y - 6));
  }
}

Image file Snake.png will be used to draw particles and can be found inside Images folder in project package. There are two version of particle, light and dark. White version will be used for even (index 0,2,4...) particles, grey for odd. This will make snake look less boring.


To use this image, we have to perform standard procedure: embed the image, create BitmapData object, and copy source image into it. Actual drawing is performed inside for loop, value of i variable decides which version of particle will be painted. Head (0) is always light, next particle is dark, light, dark, light...

To display particles on correct positions, we have to invert its vertical position (visible.y - pos.y). This is because of the difference between coordinate system for the game mechanic and screen buffer.

Size of each particle image is 12x12 pixels, distance between particles is only eight pixels (SPEED), that is why particles are overlapping. Positions stored inside partPosition array represent centre of each particle, to display particle image correctly, we have to move the location where pixels are copied, by half of the source image size (-6,-6).

Snake movement

We need to implement four functions, that will allow us to control snake movement. All functions belong to Snake class:

public function getPosition():Point
{
  return partPosition[0];
}

public function getDirection():int
{
  return direction;
}

public function setDirection(direction:int):void
{
  this.direction = direction;
}

public function move():void
{
  partPosition.pop();

  var position:Point = partPosition[0];

  switch (direction)
  {
  case NORTH:
    partPosition.unshift(new Point(position.x,position.y + SPEED));
    break;
  case EAST:
    partPosition.unshift(new Point(position.x + SPEED,position.y));
    break;
  case SOUTH:
    partPosition.unshift(new Point(position.x,position.y - SPEED));
    break;
  case WEST:
    partPosition.unshift(new Point(position.x - SPEED,position.y));
    break;
  }
}

- getPosition returns position of the snake, in fact position of the first particle (head)
- getDirection returns value of direction variable
- setDirection sets new value to direction variable
- move actually performs movement. Last particle - tail is removed (pop) and new one is inserted (unshift) at the beginning of the queue - head. Position of new particle is calculated from previous position of the head and direction of movement. When snake moves north, vertical position is increased by eight pixels (SPEED), when it moves south position is decreased by same value. Movement in east/west direction increases/decreases horizontal position.

Controlling snake movement

Now we can use these functions to update and control movement of the snake in the source code inside Game.as file. First we will update position of the snake once per frame. At the end of switch statement in Draw_Game function add new case condition:

case GAME_PLAYED:
  if (snake != null)
    snake.move();
  break;

If you start the game at this point, you won't be able to control the snake movement. Snake will move only in default direction, which is the northern direction.

We will control movement of the snake from MouseDown_Game function. Add new condition to switch block inside MouseDown_Game function:

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

    if ((Math.abs(mx - position.x) > Snake.SPEED) ||
        (Math.abs(my - position.y) > Snake.SPEED))
      if (Math.abs(mx - position.x) > Math.abs(my - position.y))
      { //check east/west
        if ((direction == Snake.NORTH) || (direction == Snake.SOUTH))
          if (mx - position.x > 0)
            snake.setDirection(Snake.EAST);
          else
            snake.setDirection(Snake.WEST);
      }
      else
      { //check south/north
        if ((direction == Snake.EAST) || (direction == Snake.WEST))
          if (my - position.y > 0)
            snake.setDirection(Snake.NORTH);
          else
            snake.setDirection(Snake.SOUTH);
      }
  }
  break;

This code is executed when player pushes the mouse button. Vertical position of mouse cursor is flipped, because the game uses different coordinate system. Snake's position and direction are stored into temporary variables.

Mouse cursor must be positioned more than eight (Snake.SPEED) pixels away from the head, otherwise direction cannot change.

If absolute horizontal distance between mouse cursor and head of the snake is greater that absolute vertical distance, direction can change to east or west only. This happens only if snake is currently moving in northern or southern direction.

If absolute vertical distance between mouse cursor and head of the snake is greater that absolute horizontal distance, direction can change to north or south, but only if snake is currently moving in eastern or western direction.

Fine tuning

If you compile and start the game you will be able to control the movement of the snake, but most probably snake will soon run out of the screen, because it moves way too fast. Game runs at 24 frames per second (by default), and snake moves by eight pixels each frame, which makes at total 192 pixels per second. Snake needs only 2.5 sec to travel from one side of the frame to another.

You can decrease the value of SPEED constant in Snake class and snake will move slowly, but particles will overlap even more and snake will look ugly.

I am using another solution: snake movement is updated once per two frame updates. This makes snake move 50% slower, but movement looks much less fluent. This solution was implemented in Game.as file. I made new global variable frameCount:

private var frameCount:int = 0;

Value of variable is increased once per frame, at the beginning of Draw_Game function.

frameCount++;

Code that updates position of the snake is changed to:

if ((snake != null) && (frameCount % 2 == 0))
  snake.move();

No comments:

Post a Comment