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.

Friday, March 5, 2010

Scoring points

In this chapter we will add three variables in the game and display their values during the game. If you think this will be a simple task, you will be amazed how much additional work is sometimes required by a simple task.



score, hiscore, lives

Those are the names of new global variables, add their definition at the top of Game.as file:

private var score:int = 0;
private var hiscore:int = 0;
private var lives:int = 3;

Value of score variable will increase each time the snake eats the egg. This value will be assigned to hiscore variable at the end of the game, but only when value of score variable is greater than value of hiscore variable. Initial value of variable lives represents the number of fatal mistakes that allowed, before the game is over. Each time the snake hits something or the egg hits the bottom edge of the frame, value will be decreased by one. Value of those variables will be displayed during the game on the top of the game frame.

Draw_ScoreLives

Draw_ScoreLives function takes care of displaying current score, highest score and number of lives that are still available. We will use the similar approach to display numbers, that we've used in the chapter 14 to display the number of current stage at the beginning of each stage. All graphics that is required to display numbers, labels and icons is inside new file ScoreLives.png.


This image is part of the project package for this chapter, you can find it inside Images folder. Image has to be embedded before it can be used. Add this statement inside Images.as:

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

Implementation of Draw_ScoreLives funciton and two definitions, belong to Game.as file:

private var scoreLivesBitmap:BitmapData = null;
private static var scoreDigitPos:Array = new Array(123, 139, 151, 165, 179, 195, 209, 224, 238, 253, 270);

private function Draw_ScoreLives():void
{
  if (scoreLivesBitmap == null)
  {
    scoreLivesBitmap = new BitmapData(290,30,true,0x00000000);
    scoreLivesBitmap.draw(new scoreLivesImg());
  }

  //score
  screenBuffer.copyPixels(scoreLivesBitmap,
                          new Rectangle(37, 0, 80, 30),
                          new Point(4,0));

  var scoreStr:String = score.toString();
  var xpos:int = 90;

  for (var i:int = 0; i < 4; i++)
  {
    var digit:int = 4 - i > scoreStr.length ? 0 : scoreStr.charCodeAt(i - (4 - scoreStr.length)) - 48;
    var digitPos:int = scoreDigitPos[digit];
    var digitWidth:int = scoreDigitPos[digit + 1] - digitPos;

    screenBuffer.copyPixels(scoreLivesBitmap,
                            new Rectangle(digitPos, 0, digitWidth, 30),
                            new Point(xpos,0));

    xpos += 15;
  }

  //hiscore
  screenBuffer.copyPixels(scoreLivesBitmap,
                          new Rectangle(0, 0, 117, 30),
                          new Point(SCREEN_WIDTH - 186,0));

  xpos = SCREEN_WIDTH - 66;

  var hiscoreStr:String = hiscore.toString();

  for (var i2:int = 0; i2 < 4; i2++)
  {
    var digit2:int = 4 - i2 > hiscoreStr.length ? 0 : hiscoreStr.charCodeAt(i2 - (4 - hiscoreStr.length)) - 48;
    var digitPos2:int = scoreDigitPos[digit2];
    var digitWidth2:int = scoreDigitPos[digit2 + 1] - digitPos2;

    screenBuffer.copyPixels(scoreLivesBitmap,
                            new Rectangle(digitPos2, 0, digitWidth2, 30),
                            new Point(xpos,0));

    xpos += 15;
  }

  //lives
  xpos = SCREEN_WIDTH / 2 - 10 - lives * 10;

  for (var i3:int = 0; i3 < lives; i3++)
  {
    if (((gameState != GAME_KILLED) && (gameState != GAME_FAILED)) || (i3 > 0) || (gameTime - int(gameTime) < 0.5))
      screenBuffer.copyPixels(scoreLivesBitmap,
                              new Rectangle(271, 0, 20, 30),
                              new Point(xpos,0));

    xpos += 20;
  }
}

At the top of the section are definitions of two global variables:
- scoreLivesBitmap is reference to BitmapData object, the copy of the embedded ScoreLives.png image.
- scoreDigitPos Array contains horizontal position of each digit in the ScoreLives.png image.

First time the Draw_ScoreLives function is called, it creates new BitmapData object and copies the content of embedded image into the object. Reference to this object is assigned to scoreLivesBitmap variable.

Inside the body of Draw_ScoreLives function are three comments: score, hiscore and lives. Each comment indicates the beginning of the code that takes care of displaying value of the variable with the same name. "Score section" is displayed on the left side of the frame, "Hi-Score section" is on the right and "Lives section" in the middle.

The code that draws "Score section" and "Hi-Score section" is very similar. Even the same label from the source image, is used to draw "Score" and "Hi-Score" label in the game. Displayed value consist of four digits, each digit is drawn inside for loop from left to right. Just before the loop starts, new String object is made from value of the variable. If the String object has less than four digits, zeros are displayed in front of the actual number. Values of digitPos and digitWidth are calculated from horizontal position of the digits in the source image, defined in scoreDigitPos Array. When displaying zero, digitPos gets the value of first element in the Array and digitWidth is the difference between position of character one and zero. In the copyPixels statement, both values are used to define the area (Rectangle) of required digit in the source image.

Value of lives variable is displayed with the icons on the top-middle section of the game frame. Each icon is drawn inside the for loop, the if condition before copyPixels statement, makes left icon to blink, when the player makes the mistake.

Function Draw_ScoreLives will be called once per frame from Draw_Game function. Add this statement behind the code that draws background, egg and the snake (before the switch statement):

Draw_ScoreLives();

Now you can compile and play the game. But no matter what you do, number of scored points and icons will not change.

Scoring points, loosing lives...

At the beginning of the game, we have to reset values of score and lives variable. The New_Game function is perfect place to do that:

score = 0;
lives = 3;

When the snake eats the egg, value of score variable will be increased by 5. Collision between the snake and the egg is checked in CheckCollision function. Add the following statement after snake.append(); statement:

score += 5;

You can select a different number, if you like, but big numbers will require more digits to display score/hiscore points.

When player makes the fatal mistake, value of lives variable will decrease by one. When it reaches zero, the game is over. Replace the current version of Restart_Game function with:

private function Restart_Game():void
{
  lives--;

  if (lives == 0)
  {
    if (score > hiscore)
    {
      hiscore = score;

      StoreData(HISCORE);
    }

    state = GAME_MENU;
    ChangeState_GameMenu(TITLE_SHOW);
  }
  else
  {
    scrollPos = 0;

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

    ChangeState_Game(GAME_FADE_IN);
  }
}

Restart_Game function was implemented in chapter 19. This function is called few seconds after player made a mistake. Previous version just made a new Snake and Egg objects, reseted scrollPos variable and restarted the game (state GAME_FADE_IN). New version does all the same, but only if value of lives variable, which is decreased by one at the start of the function, is more than zero.

If value is zero, it is game over. First, value of score variable is compared to value of hiscore variable. If values of score variable is greater, the player just broke new record, value of score variable is assigned to hiscore variable and StoreData function is called. This function will be implemented at the end of the chapter. Finally, GAME_MENU state becomes active and eventually, program displays title screen and game menu.

When the player enters game menu, mouse cursor needs to be replaced. Add new statement at the end of Draw_GameMenu function in GameMenu.as:

SetMouseCursor(0,false);

Load/StoreData

When the player sets the new score, value of hiscore variable has to be stored permanently, and available the next time, when the player starts the game. Shared Objects are the perfect place to store this kind of data. Data is actually stored on the local computer, within the user's home directory.

We called StoreData function from Reset_Game function, to store the value of hiscore variable. Add the implementation of this function at the end of file SnakesAdventure.as:

private static const HISCORE:int = 1;

private function StoreData(what:int):void
{
  var so:SharedObject = SharedObject.getLocal("SnakesAdventure");

  switch (what)
  {
  case HISCORE:
    so.data.hiscore = hiscore;
    break;
  }

  so.flush();
}

In front of StoreData function is definition of one global constant HISCORE (in the following chapters we will add more of them). This constant can be used in StoreData function call, as value of what parameter. If HISCORE constant is used as a parameter value, function knows that value of hiscore variable needs to be stored.

Function getLocal from SharedObject class, returns the reference to SharedObject named "SnakesAdventure". Value of hiscore variable is stored into data collection, under the hiscore attribute. If the attribute with this name does not exist, it is created; in case it exits, current value is replaced with new one. After all attributes are set, function flush writes data into file.

LoadData function will load the data from "SnakesAdventure" SharedObject and assign values of attributes to variables with the same name.

private function LoadData():void
{
  var so:SharedObject = SharedObject.getLocal("SnakesAdventure");

  if (so.size > 0)
  {
    if (so.data.hiscore != null)
      hiscore = so.data.hiscore;
  }
}

Function getLocal always returns the reference to SharedObject object with name "SnakesAdventure". If it does not exist, it creates new - empty one. If attribute size is equal to zero, there is no data and we can skip the loading procedure. Existence of hiscore attribute in data collection has to be checked, before its value can be assigned to hiscore variable.

Init function at the beginning of the file SnakesAdventure.as is a good place to call LoadData function:

LoadData();

In the initialzation phase of the program, LoadData function is called and value of hiscore attribute, from the "SnakesAdventure" SharedObject, is assigned to hiscore variable. If the attribute does not exist, variable hiscore has default value - 0. When the player breaks new record, value of score is assigned to hiscore variable (in Restart_Game function) and stored into SharedObject. At the end, all data in the SharedObject is written into file.

Shared objects are application dependant: if you test the game in two different browsers, for example FireFox and Opera, both of them have its own "SnakesAdventure" SharedObject.

No comments:

Post a Comment