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.

Tuesday, February 9, 2010

Snake - advanced version

In the previous chapter we implemented new functionality that displays and controls the movement of the snake. It probably required a lot of your time to understand how things work and maybe some of you were a bit disappointed with the look of the snake. Some people like this old-school look, others will preferer that snake character actually looks like snake.

It is best that you start the game and see for yourself how will the new version of snake look like, after we finish 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 the snake runs out of the screen, just reload the page.




If you do not like this new look, skip this chapter and rather spent some time to improve the graphics of the snake from the previous chapter. Otherwise, keep reading.

Snakes new groove

To implement advanced version of snake, we need new graphics. New version of file Snake.png can be found inside Images folder in project package for this chapter. As you can see on the (enlarged) image bellow, it now contains images of different parts of snakes body in all possible directions: horizontal and vertical version of the body, heads and tails for all directions and image that fills the gap between two parts with different directions.


Implementation of advanced version of the snake will require a lot of changes in the existing source code. Fortunately, most of the changes will be made in the Snake class (Snake.as). In the Game.as we have to only change the value of length parameter in Snake constructor:

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

If you slowed down the movement of the snake at the end of previous chapter, by calling move function once per two frames, it is best that you change the code back to:

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

Because the value of SPEED constant has been decreased to four, the move function changes the position of the snake by four pixels only. Updating the position of the snake once per two frames will really look anoying. Change the SPEED constant in the Snake class to:

public static const SPEED:int = 4;

In the previous version of the snake, position of each part was stored to Point object and all Point objects were kept inside partPosition Array. For advanced version, we also need to store direction of each part, otherwise we cannot display correct image of tail and image that fills the gap between two parts with different directions. Directions of all parts will be kept inside partDirection Array.

There is one more thing we need, to display body parts correctly. I call this data a phase, value of all is kept inside partPhase Array. Phase can have value from 0 to 8, each value is used to display a different part of snake body.

Change the section at the beginning of Snake class, that defines class members to:

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

There is one new member that I haven't mentioned, yet. Variable newDirection will be used to store the value of new direction in setDirection function. In previous version, we immediately changed the value of direction and consequently snake immediately changed the direction of movement. In the advanced version, we have to wait until the time is right, before we can change the direction. The right time is when phase value of leading part (head) equals 0.

Snake class constructor now looks like this:

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));
    partDirection.push(direction);
    partPhase.push(i % 9);
  }
}

Position of all parts is set the same way as in previous version, direction for all parts is set to NORTH, phase values are set in following order 0, 1, 2, 3, 4, 5, 6, 7, 8, 0, 1, 2, 3...

Implementation of functions getPosition and getDirection has not changed, on the other hand, setPosition function now stores new direction in newDirection variable instead directly to direction variable:

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

Implementation of move function has been completely changed and is now almost hundred lines long. I will not show complete source code here, I will try to explain how it works on the section, that moves snake in northern direction. You can find complete source code in Snake.as file in project package file.

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

  var position:Point = partPosition[0];
  var phase:int = partPhase[0];

  switch (direction)
  {
  case NORTH:
    if ((newDirection != direction) && (phase == 0))
    {
      direction = newDirection;

      if (direction == EAST)
        partPosition.unshift(new Point(position.x + 7,position.y + 8));
      else
        partPosition.unshift(new Point(position.x - 6,position.y + 8));

      partPhase.unshift(0);
    }
    else
    {
      partPosition.unshift(new Point(position.x,position.y + SPEED));
      partPhase.unshift(((phase - 1) < 0 ? 8 : phase - 1) % 9);
    }
    break;
    .
    .
    .
  }

  partDirection.unshift(direction);
}

Function first removes (pop) data for the last part of the snakes body from all Arrays, position and phase value of leading body part is stored to temporary variables.

In case the snake moves in the northern direction, it checks if value of newDirection variable is different from value of direction variable, which means that player recently changed the direction of the snake. When phase value of leading part equals 0, direction of movement can change. Position of new part is not increased by four in vertical direction, but is changed according to size of the image that fills the gap between two parts with different directions. Phase value of new part is set to zero.

If direction of movement hasn't changed, position of new part is increased by four (SPEED) in vertical direction. Phase value of new part is decreased by one or set to 8 if previous value was zero. Finally, direction of new part is inserted at the beginning of Array.

Implementation of drawing

The draw function has become much more complicated. Now we have to use different parts of the source image, to display head, tail and body. Again, I will show only part of the drawing routine and explain how it works. Complete source code of draw function can be found in Snake.as file in project package file.

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

private var bitmap:BitmapData = null;

private var northHeadOffset:Array = new Array(0,2,3,3,1,-1,-3,-3,-2);
...

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

  for (var i:int = partPosition.length - 1; i >= 0; i--)
  {
    var pos:Point = partPosition[i];
    var direction:int = partDirection[i];
    var phase:int = partPhase[i];

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

      switch (direction)
      {
      case NORTH:
        screenBuffer.copyPixels(bitmap,
                                new Rectangle(38,51,15,17),
                                new Point(pos.x - 7 + northHeadOffset[phase],visible.y - pos.y - 13));
        break;
      ...
      }
    }
    else
    if (i == partPosition.length - 1) //tail
    {
      switch (direction)
      {
      case NORTH:
        screenBuffer.copyPixels(bitmap,
                               &nbspnew Rectangle(80,2,11,10),
                               &nbspnew Point(pos.x - 5 + northHeadOffset[phase], visible.y - pos.y - 3));
        break;
        ...
      }
    }
    else //body
    {
      if ((direction == EAST) || (direction == WEST))
        screenBuffer.copyPixels(bitmap,
                                new Rectangle(18 + phase * 4,0,4,19),
                                new Point(pos.x - 2,visible.y - pos.y - 9));
      else
        screenBuffer.copyPixels(bitmap,
                                new Rectangle(0,18 + phase * 4,19,4),
                                new Point(pos.x - 9,visible.y - pos.y - 2));
    }

    var prevDirection:int = i < partPosition.length - 1 ? partDirection[i + 1] : direction;

    if (prevDirection != direction)
      switch (prevDirection)
      {
      case NORTH:
        if (direction == EAST)
          screenBuffer.copyPixels(bitmap,
                                  new Rectangle(72,51,10,11),
                                  new Point(pos.x - 12,visible.y - pos.y - 5));
        else
          screenBuffer.copyPixels(bitmap,
                                  new Rectangle(82,51,10,11),
                                  new Point(pos.x + 2,visible.y - pos.y - 3));
        break;
        ...
      }
}

Source code that embeds Snake.png and creates BitmapData object from the source image has not changed, only width and height of source image has been changed.

Parts of the snake are now copied to screen buffer in reverse order: tail first, than body and finally the head. To display the head and tail in right direction, we have to use appropriate area of source image. Because the position of body changes vertically/horizontally, we need to adjust the position of the head and tail with predefined values from northHeadOffset Array. Different parts of the body are displayed according to phase value.

Finally, the direction of previous part is compared to direction of the part in the process. If they have different direction, we have to fill the gap. Gap is filled with suitable part of the image in bottom right section of Snake.png.

No comments:

Post a Comment