23 November 2012

Okay, this is the last series… And sorry for being very late to write the article. Got a little bit busy at the early November…

citrus



=====================
If you want to read the other part, here are all the links:
1. Game Structure
2. Adding Screen Transition
3. Head-Up Display
4. Working with Data
5. Creating Level using Tiled Map Editor
=====================

As you can see above, Citrus Engine got a new logo, pretty cool logo I think… And seems that the future of this engine will be very bright as now they joined with Starling, so the engine will got more supporter and contributor.

And now CE website also fully  redesigned, and it looks better now. You can look for almost everything there, from tutorial to documentation. Definitely better than some months/years ago.

So I think my article here will not relevant anymore as you’d better to look for support from more competent people on this engine. Just go to the new community, and you’ll get better answers.
But nothing to worry, I still want to finish the series, so lets get started.

Actually, CE already have their own class to handle game data, called AGameData.as. But cause I’m a little bit conservative when developing my game, it’ll always better for me to use my own tools than use built-in feature from the engine. So now I’ll discus how to create a class to handle your game data. Maybe this tutorial will be useful if you want to incorporate a data class for your non-CE games.

I will not discuss about the AGameData class but actually it’s no different with what I usually use for managing game data. It’s just a property with signal that will be dispatched when its value changed. Pretty simple.

Nuff said, from the sample we’ve built before, I create three data class:
1. Player Data, will hold current players all-time stats. From money, score, enemy killed count, etc.
2. Level Data, store which level are unlocked and which one are still locked. And probably can store a level best score also.
3. CurrentLevelData. Yep, I have an ugly class name, it’s seems ambiguous. The purpose of this class is for a temporary data storage when the player plays a level. When he complete the game, simply add the property values to PlayerData. Otherwise, when the player lose or exit the level, then they will not gain anything.

Since my data class isn’t integrated with the engine, then I use a singleton. So it can be instantiated everywhere while still referring from the same object.

Just be cautious when using singleton. Some people said that it’s a part Design Pattern, the other one called it Anti-Pattern, and there are whom called singleton as an evil. I couldn’t agree more, but I just found out that singleton + signal could be a root of all evil, if the signal is not removed properly…

Alright, let’s look to the code…

PlayerData.as
package src.data 
{
 import flash.net.SharedObject;
 
 public class PlayerData 
 {
  private static var instance:PlayerData;
  
  private var score:int = 0;
  private var money:int = 0;
  private var death:int = 0;
  private var enemies_killed:int = 0;
  
  public static const SCORE:String = "score";
  public static const MONEY:String = "money";
  public static const DEATH:String = "death";
  public static const ENEMIES_KILLED:String = "enemies_killed";
  
  private var sharedObject:SharedObject;
  
  public function PlayerData(enforcer:SingletonEnforcer) 
  {
   sharedObject = SharedObject.getLocal("saved_data");
  } 
  
  public static function getInstance():PlayerData
  {
   if (instance == null)
   {
    instance = new PlayerData(new SingletonEnforcer);
   }
   
   return instance;
  }
  
  public function loadData():void
  {
   if (!sharedObject.data.hasOwnProperty("score"))
   {
    return;
   }
   
   score = sharedObject.data.score;
   money = sharedObject.data.money;
   death = sharedObject.data.death;
   enemies_killed = sharedObject.data.enemies_killed;
  }
  
  public function saveData():void
  {
   sharedObject.data.score = score;
   sharedObject.data.money = money;
   sharedObject.data.death = death;
   sharedObject.data.enemies_killed = enemies_killed;
   
   sharedObject.flush();
  }
  
  public function resetData():void
  {
   score = 0;
   money = 0;
   death = 0;
   enemies_killed = 0;
   
   saveData();
  }
  
  public function increaseData(data:String, value:int):void
  {
   this[data] += value;
  }
  
  public function decreaseData(data:String, value:int):void
  {
   this[data] -= value;
  }
  
  public function getData(data:String):int
  {
   return this[data];
  }
 }
}

class SingletonEnforcer
{
 
}

Pretty straight forward, we create 4 variables: score, money, death, & enemies_killed. And bunch of string constants, so we can call them by their name.

I also use SharedObject to manage the save data. For detailed explanation about that, you’d better read this article. It’s very well explained on that site.

I won’t explain about the Singleton class structure, so lets moving forward to the relevant methods.

loadData(). Yep, this method is just used to load saved data. Simply check that the saved data already have property called “score” or not, by using hasOwnProperty() method. If yes then load it, otherwise just skip the process.

saveData(). To save the game data to shared object, simply pass the current value to them, and call flush() method in the end to write new data to player disk.

resetData(). Do I really need to explain this? XD

increase, decrease, and getData(). This is when the name constants are useful. So there is no need to create a method for each variable, eg: increaseScore(), decreaseScore(), increaseMoney(), decreaseMoney(), etc. We can use one single method for all.

LevelData.as is actually has no difference from PlayerData.as. It just contains level_1 to level_n Boolean variables, that will set to true when the players complete previous level.

So I’ll skip them, and jump to CurrentLevelData.as instead, as this class shows how to use data with signals.

package src.data
{
    import org.osflash.signals.Signal;
    
    public class CurrentLevelData
    {
        private static var instance:CurrentLevelData;
        
        private var level:int = 1;
        
        private var score:int = 0;
        private var money:int = 0;
        private var death:int = 0;
        private var enemies_killed:int = 0;
        
        private var health:int;
        private var maxHealth:int = 100;
        
        public var onDead:Signal;
        public var onDataChange:Signal;
        public var onHealthChange:Signal;
        
        public function CurrentLevelData(enforcer:SingletonEnforcer)
        {
            onDead = new Signal();
            onDataChange = new Signal(String, int);
            onHealthChange = new Signal(int);
        }
        
        public static function getInstance():CurrentLevelData
        {
            if (instance == null)
            {
                instance = new CurrentLevelData(new SingletonEnforcer);
            }
            
            return instance;
        }
        
        public function resetData():void
        {
            score = 0;
            money = 0;
            enemies_killed = 0;
            death = 0;
            health = maxHealth;
        }
        
        public function increaseData(data:String, value:int):void
        {
            this[data] += value;
            
            onDataChange.dispatch(data, this[data]);
        }
        
        public function increaseHealth(value:int):void
        {
            if (health + value < maxHealth)
            {
                health += value;
                
                onHealthChange.dispatch(health);
            }
            else
            {
                health = maxHealth;
            }
        }
        
        public function decreaseHealth(value:int):void
        {
            if (health - value > 0) 
            {
                health -= value;
                
                onHealthChange.dispatch(health);
            }
            else
            {
                health = 0;
                
                onDead.dispatch();
            }
        }
        
        public function setLevel(value:int):void
        {
            level = value;
        }
        
        public function getLevel():int
        {
            return level;
        }
        
        public function getData(data:String):int
        {
            return this[data];
        }
        
        public function getHealth():int
        {
            return health;
        }
        
        public function getMaxHealth():int
        {
            return maxHealth;
        }
    }
}

class SingletonEnforcer
{
    
}


As a temporary data storage for certain level, it contains same property as PlayerData.as: score, money, enemies_killed, & death. But it also contains health property. Well, it’s your choice to put health property on the Hero class itself or on the separate class. But for this example, I put health property outside the Hero class.

And I create some signals:

onDataChange, dispatched when there is a value changes for the score, money, etc. So we can update the game’s HUD.

onDead, dispatched when the player dead.

onHealthChange, dispatched when the player’s health value changed.
For the methods, I think it’s pretty clear. Nothing to explain further.

After we create the data classes, now start integrating them to the game…
On the GameState.as constructor, simply instantiate the CurrentLevelData:

currentLevelData = CurrentLevelData.getInstance();
currentLevelData.resetData();
currentLevelData.setLevel(level);
currentLevelData.onDataChange.add(dataChangeHandler);
currentLevelData.onHealthChange.add(healthChangeHandler);
currentLevelData.onDead.add(deadHandler);


Nothing too special, just reset the data first, and add some listeners to the signals.

Lets take a look on the listener method that will update it's value when there are changes in the data property values:

private function dataChangeHandler(data:String, value:int):void
{
     if (data == PlayerData.SCORE)
     {
          hud.scorex.text = currentLevelData.getData(PlayerData.SCORE);
     }
     else if (data == PlayerData.MONEY)
     {
          hud.moneyx.text = currentLevelData.getData(PlayerData.MONEY);
     }
 }


Since the HUD only contains two properties to show (money and score), so we need to make sure the returned value are from both property. And then simply change the HUD text for each property.

For the player health, we need a special treatment since it’s a dynamic HUD that use CitrusSprite instead of Flash MovieClip. So to access the graphic of that HUD, we need to do some work. Well, actually on my sample here it has been simplified to two lines of code.

private function healthChangeHandler(value:int):void
{
     var hero:CustomHero = getObjectByName("hero") as CustomHero;
     var hpBar:MovieClip = SpriteArt(view.getArt(hero.getHPBar())).content as MovieClip;
            
     hpBar.barx.scaleX = currentLevelData.getHealth() / currentLevelData.getMaxHealth();
}


First we need to get a reference to the hero using CE getObjectByName() method. After that we need the Movie Clip version of the HUD (HP Bar) as you can see on second line.

If we already have the reference to the hpBar Movie Clip, then we can simply scale it based on percentage of the player’s health.

REMOVE THE SIGNAL LISTENER!!!

Hell yeah, I consider that this is a very important as you'll get into a nightmare if you doesn't remove your signal listener.

So what exactly will happen if we don't remove the listener? Since it's a singleton so the signal never removed, and this also cause the parent/caller object will not be garbage collected. Although we create and override it with new object that also instantiate new signal listeners, the old one still listen to any events.  When an event dispatched, then it will throw an epic Null Object Error.

currentLevelData.onHealthChange.remove(healthChangeHandler);
currentLevelData.onDead.remove(deadHandler);
currentLevelData.onDataChange.remove(dataChangeHandler);
currentLevelData = null; 

Well, seems that there are nothing more to write here. So here are the finished sample. No difference from previous one… lol



And as always, the source code:

0 comments:

Post a Comment