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.

Sunday, February 28, 2010

Background and scrolling

In this chapter, we will make three changes in the game:
- single colour in the background will be replaced with an image
- vertical scrolling will be implemented
- instead of predefined positions of the eggs we will use random positions

Because of the background image, the graphics will look a lot less boring, vertical scrolling will improve the gameplay and random positions will make game less predictable.

Background

I made this background image from the grass texture from one of my older games. The image is part of the project package for this chapter, you can find it inside Images folder under the name Background.jpg.


So far, all images in the project were in PNG format, because it supports alpha transparency and employs lossless data compression. JPEG image format was so much more appropriate in this case, that I had to make an exception. File size of the same image in JPEG format is almost five times (130KB) less than if PNG format is used (700+KB).

Again, we will put all the functionality that takes care of embedding, drawing... into it's own class. Create new text file and store it, under the name Background.as, into folder Classes. Copy the following content into file:

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

  public class Background
  {
    private static const SCREEN_WIDTH:int = 480;
    private static const SCREEN_HEIGHT:int = 480;
    private static const IMAGE_HEIGHT:int = 640;

    [Embed(source="../Images/Background.jpg")] private var backgroundImg:Class;

    private var bitmap:BitmapData = null;

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

      screenBuffer.copyPixels(bitmap,
                              new Rectangle(0,0,SCREEN_WIDTH,SCREEN_HEIGHT),
                              new Point(0,0));
    }
  }
}

Because constants defined in SnakesAdventure.as are out of the scope of this package/class, SCREEN_WIDTH/HEIGHT constants had to be defined here, too. Background image width and game frame width are the same, but image height and game frame height are not. Image height is defined with IMAGE_HEIGHT constant. I made background image that is higher than game frame, to reduce repetition in vertical scrolling.

This class does not include definition of constructor, default constructor will be used when the class is instantiated. Implementation of draw function has nothing that we haven't seen before: BitmapData object is made from embedded image (only the first time) and pixels are copied from BitmapData object into screen buffer.

Now we can use Background class to display background image during the game. All we have to do is to define background variable, instantiate the Background class and call it's draw function once per frame.

Put background variable definition before definition of snake variable, at the beginning of Game.as:

private var background:Background = null;

Background object has to be made at the beginning of the game. Add this statement inside New_Game function:

background = new Background();

Background has to be drawn before the snake and the egg. Previously, background colour was painted with screenBuffer.fillRect... statement, which can now be replaced with:

background.draw(screenBuffer,new Rectangle(0,SCREEN_HEIGHT,SCREEN_WIDTH,SCREEN_HEIGHT));

Vertical scrolling

Because this game is called Snake's adventure, players will most certainly expect more that just a snake chasing the eggs on the static background. With implementation of vertical scrolling we will create the sense that snake is constantly moving in the northern direction. Of course, to create the sense of real adventure, the game needs much more than just vertical scrolling.

Implementation of vertical scrolling will not require so much work, because we have already done most of the work. If you remember from previous chapters, draw functions in all classes, already have visible parameter, which represent the area of the game world that is visible at the moment the function is executing. Snake and Egg classes already take into account the coordinates of visible area, when copying source images into screen buffer (visible.y - position.y).

Vertical scrolling will be active only while the game is played (GAME_PLAYED state). Once per frame, visible area of the game world will be moved by one pixel in the northern direction.

OK, let's start with the implementation. First, we need new variable, value of variable will be increased once per frame. Add new variable after definition of level variable at the top of Game.as:

private var scrollPos:int = 0;

Value of variable has to be reset at the beginning of the level. Add this statement into New_Game and Restart_Game functions:

scrollPos = 0;

Vertical position of visible area needs to be increased by value of scrollPos variable. Replace the second parameter in all three draw statements at the top of Draw_Game function, with:

new Rectangle(0,SCREEN_HEIGHT + scrollPos,SCREEN_WIDTH,SCREEN_HEIGHT)

If you applied the changes correctly, the source code, should look like this:

background.draw(screenBuffer,
                new Rectangle(0,SCREEN_HEIGHT + scrollPos,SCREEN_WIDTH,SCREEN_HEIGHT));

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

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

Value of scrollPos variable will be increased inside Draw_Game function only while GAME_PLAYED state is active. Add the following statement inside the switch statement at the end of case GAME_PLAYED (before break):

scrollPos++;

Although we have not finished the implementation, you can now compile and run the game to see how it works. Background is still static, but the egg is moving in the southern direction, which is OK. And so is the snake, but you can only notice this when it moves in the horizontal direction.

Now, open Background.as file and replace copyPixels statement in the draw function, with the code that supports vertical scrolling:

var start:int = IMAGE_HEIGHT - visible.y % IMAGE_HEIGHT;
var end:int = start + SCREEN_HEIGHT > IMAGE_HEIGHT ? IMAGE_HEIGHT - start : SCREEN_HEIGHT;

screenBuffer.copyPixels(bitmap,
                        new Rectangle(0,start,SCREEN_WIDTH,end),
                        new Point(0,0));

if (end < SCREEN_HEIGHT)
  screenBuffer.copyPixels(bitmap,
                          new Rectangle(0,0,SCREEN_WIDTH,SCREEN_WIDTH - end),
                          new Point(0,end));

This may be a little bit hard to understand, but let me try to explain why we have to copy pixels from source image into screen buffer, in two steps. Background image is by 160 pixels higher that visible area. In some cases source image fits inside the visible area, but in some cases doesn't.

At the start of the game, Rectangle(0,160,480,480) defines the area of source image that is copied into screen buffer. Vertical position is decreasing by one per frame and after 160 frames, area from the source image, defined by Rectangle(0,0,480,480), is copied. At this moment, background image is visible from top to bottom minus 160 pixels.

When the next time Background.draw function is called, background image is copied into screen buffer in two steps. First, the bottom row of source image is copied on the top of screen buffer. The rest of the screen buffer is filled with area from the source image, defined by Rectangle(0,0,480,479). And so on...

Unfortunately, grass image (Background.jpg) that is used for the background, doesn't help much to understand how this code works. Maybe it would help, if you temporary use a different image for the background. Rename Background.jpg to, let's say, Background2.jpg. Resize one of your photos to 480x640 pixels and add it to project inside Images folder under the name Background.jpg.

If you compile and run the game, vertical scrolling should work fine, but after few seconds you will notice, that controls and collision detection do not work correctly.

Let's fix this. Change the statement, that flips screen coordinates to game world coordinates in SetGameCursor and MouseDown_Game function, from:

my = SCREEN_HEIGHT - my;

to

my = SCREEN_HEIGHT + scrollPos - my;

This should fix problems with controls. Code that detects collision between the head of the snake and top and bottom edge of the frame will be fixed the same way. Vertical position of both edges has to be increased by the value of scrollPos variable. Change the code in CheckCollision function to:

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

Implementation of vertical scrolling is now complete, but one problem still remains: egg positions specified in Egg.position Array became useless. You can take into account the scrolling and adjust vertical positions, but after first iteration, coordinates will still be useless. We have to implement the mechanism, that will set random position of new egg, according to vertical position of visible area.

CreateRandomEgg

CreateRandomEgg is the new function in Game.as file, that creates new egg on random position inside the visible area of game world. Add implementation of the function into Game.as file:

private function CreateRandomEgg(initial:Boolean):Egg
{
  var x:int = (int)(Math.random() * (SCREEN_WIDTH - 40)) + 20;
  var y:int = initial ? (int)(Math.random() * (SCREEN_HEIGHT / 2)) + SCREEN_HEIGHT / 2 :
      (int)(Math.random() * (SCREEN_HEIGHT * 2 / 3)) + SCREEN_HEIGHT / 3 + scrollPos;

  for (var i:int = 0; i < snake.getLength(); i++)
  {
    var partPosition:Point = snake.getPartPosition(i);

    if (Point.distance(partPosition, new Point(x,y)) < 30)
      return CreateRandomEgg(initial);
  }

  return new Egg(x,y,initial);
}

Function requires initial parameter that we already used in Egg class constructor. Variables x and y represent horizontal and vertical position of new egg. Both values are random, horizontal position can range from 20 to SCREEN_WIDTH - 20. Offset of 20 pixels ensures, that egg is not positioned to close to the left or right edge of the frame.

Vertical position of first (initial) egg is randomly selected in the upper section of visible area. Other eggs can be positioned a little be lower on the screen.

Position of new egg cannot be too close to snake's body. Code inside the for loop, checks if distance between the egg and each body parts is outside the range of 30 pixels. If all body parts are outside the range, new Egg object is created and returned.

Implementation of CreateRandomEgg function calls getPartPosition function from Snake class that is currently unimplemented. And so is the Egg class constructor that is used her. We will implement both of them later.

New Egg objects are made inside New_Game, Restart_Game and Draw_Game function. Replace all statements that creates Egg object from:

egg = new Egg(true);

with

egg = CreateRandomEgg(true);

Just be careful that you don't mix up true and false values. Before you can compile the project, we have to implement getPartPosition function and change the Egg class constructor.

Add implementation of getPartPosition function inside the Snake class (file Snake.as):

public function getPartPosition(part:int):Point
{
  return partPosition[part];
}

Replace the current version of Egg class (file Egg.as) constructor with:

public function Egg(x:int,y:int,initial:Boolean)
{
  if (initial)
    phase = 10;
  else
    phase = 1;

  position = new Point(x,y);
}

The code is much more simple now, because CreateRandomEgg function does most of the work. You can also remove definitions of positions Array and nextPosition variable, because it won't be needed any more.

You can compile and run the game now. As you can see, we've made a big progress in this chapter. But there is still one thing missing: the game does not check if the egg hits the lower edge of the game frame. If the egg is not eaten in time, it is game over.

CheckCollision

Improved version of this function now looks like this:

private function CheckCollision():Object
{
  var headBounds:Rectangle = snake.getBounds(0);

  //check egg and the bottom of the screen
  var eggBounds:Rectangle = egg.getBounds();

  if (eggBounds.y - eggBounds.height <= scrollPos)
    return egg;

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

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

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

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

  return null;
}

New version is not much different from previous version but there are two important differences:
- function first checks if bottom section of egg icon is outside the game frame
- function now returns Object instead of Boolean. If there is no collision, that causes the end of the game, function returns null. If head of the snake hits the edge of the screen or its own body, function returns snake object. If egg hits the lower edge of the screen, function returns egg object.

CheckCollision is called from Draw_Game function once per frame. Because new version of this function returns a different type, we have to also modify the code inside the Draw_Game function (inside case GAME_PLAYED):

var obj:Object = CheckCollision();

if (obj is Snake)
  ChangeState_Game(GAME_KILLED);

if (obj is Egg)
{
  egg.blink();
  ChangeState_Game(GAME_FAILED);
}

When the Snake object is returned, state GAME_KILLED becomes active and animation of dying snake is displayed. If Egg object is returned, we have to perform different animation. Egg will blink during GAME_FAILED state, which will be active for only few seconds.

Add the definition of GAME_FAILED constant to other constant at the top of Game.as

private static const GAME_FAILED:int = 6;

Currently, the GAME_FADE_OUT constant has value 6, change its value to 7. The following section belongs before case GAME_FADE_OUT statement inside Draw_Game function:

case GAME_FAILED:
  if (!egg.blink())
    ChangeState_Game(GAME_FADE_OUT);
  break;

While the GAME_FAILED state is active, blink function of Egg object is called until it returns value false. Function blink will take care of blinking egg.

This new animation requires new variable in Egg class (file Egg.as):

private var blinking:int = 0;

This variable will be used as a counter, that will be set only the first time blink function is called, and decreased by one, during the execution of the function. According to the value of the variable, value of phase variable is set. Value of phase variable is used to display the egg icon with different levels of transparency.

Add the implementation of blink function inside the Egg class:

public function blink():Boolean
{
  if (blinking == 0)
    blinking = 70;

  blinking--;

  phase = blinking % 10;

  if (blinking / 10 % 2 == 1)
    phase = 10 - phase;

  return (blinking != 0);
}


No comments:

Post a Comment