Citrus Engine + Dragon Bones: The Cyborg


As you know that Citrus Engine and Starling seems to be officially support Dragon Bones as their main skeletal animation library. It’s Dragon Bones that have their sub-forum on Starling forum, not other skeletal animation tools/libs like Brashmonkey Spriter and Esoteric Spine.

Dragon Bones Citrus Engine

But, even though Dragon Bones have their own place at the community, it still goddamn hard to find a good & complete tutorial how to get the best of that tool. What you get is a very basic getting started tutorial, also some complicated example files.  So you’ll also need to read the documentation and searching on the forum.
And most of them are for Starling, not Citrus Engine specific. Well, actually, everything that work on Starling will also work on CE.

I can only found one good CE + Dragon Bones tutorials, here at PseudoSamurai. If it’s your first time using Dragon Bones, then you’d better follow that link. Since I’ll post more advanced tutorial here.
Well, not that advanced. Just some modification of the DB “Cyborg” example .fla file and how to implement it on CE.

Animation Setup
As I already stated above, We’ll use the DB Cyborg.fla file. Get it here: DB Sample.
The .fla file is a example how to setup character that using nested armature to create a character that can switch its weapon.
But with some modification. I set the animation label to be match with CE animation setup: idle, walk, jump, and duck. And I remove some unnecessary animation frames.

Citrus Engine Dragon Bones

Actually, if you know how to setup DB armature for CE, you can use this DB character for your hero. And if you take a look at Example_Cyborg_SwitchWeapon.as file, then you can implement a switching weapon feature by yourself. But for the sake of this article,  I’ll just write it here and add some minor additions: blinking eye & shooting animation.

Blinking Eye
First thing first, open the Head movieclip until it show its timeline. You’ll see three layers, each contains helmet, face, and eye. Convert all these objects to MovieClip.
By converting all graphics to MovieClip then it will marked as Skeleton/Armature by Dragon Bones. So it have independent animation. In this case it's the blinking eye.
And then animate the eye. It just reducing the eye’s height, nothing more. Pretty simple.

Citrus Engine Dragon Bones Tutorial
the middle layer is the eye layer


Shooting Animations
Open the armOutside movieclip. You can see each weapon are separated into frames. There are 4 weapons: assault rifle, sniper rifle, machine gun, and rocket launcher. Assault rifle at frame 1 with label weapon1, sniper rifle at frame 2 with label weapon2, etc.

Now add additional keyframe for each weapon frame. For example, for a frame  labeled with weapon1, create a new keyframe with weapon1shoot label. Then on the stage, simply move and rotate the arm and the gun to create a shooting effect. Do the same thing to other weapon frames.

And since DB also support animation blending,  no need to set up more complex and tween animation. DB will do the rest.

Citrus Engine Dragon Bones Tutorial

Do the same thing for armInside movieclip too.

Open the Dragon Bones extension panel. Then hit the ‘import’ button. Check the animation, adjust everything until you get the desired effect.
Also make sure that there are 4 skeletons: cyborg, armOutside, armInside, and Head.
Originally, this DB .fla example will only have 3 skeletons cyborg, armOutside, and armInside, but we've convert the head's graphics into MovieClip, and add the blinking animation, so now you can see, it become a DB skeleton.

Open the Head skeleton. Set the Play Times to 0 to make it loop. So the eye will blink endlessly and continuously.
The other values are up to you to set the blinking speed & duration, but my settings are like this:

Citrus Engine Dragon Bones Tutorial

Make sure everything is set up properly. Then export it to PNG + XML file.
Extract the zip file.
And start coding.


To the Code
Create a Starling CE project on your IDE. Setup the Main and State class as well. I’m using Nape here, but that shouldn’t be a problem if you using Box2D.

I’ll post the complete code of the State class, and then the explanations is after that.

package  
{    
    import citrus.core.starling.StarlingState;    
    import citrus.objects.platformer.nape.Hero;    
    import citrus.objects.platformer.nape.Platform;    
    import citrus.physics.nape.Nape;    
    import citrus.view.starlingview.StarlingArt;    
    import dragonBones.Armature;    
    import dragonBones.Bone;    
    import dragonBones.events.AnimationEvent;    
    import dragonBones.factorys.StarlingFactory;    
    import dragonBones.objects.XMLDataParser;    
    import flash.events.TimerEvent;    
    import flash.geom.Point;    
    import flash.geom.Rectangle;    
    import flash.ui.Keyboard;    
    import flash.utils.Timer;    
    import starling.textures.Texture;    
    import starling.textures.TextureAtlas;    
  
    public class GameState extends StarlingState    
    {    
        [Embed(source="../assets/texture.png")]    
        private static const CYBORG_PNG:Class;    
        
        [Embed(source="../assets/texture.xml",mimeType="application/octet-stream")]    
        private static const CYBORG_TEX_DATA:Class;    
        
        [Embed(source="../assets/skeleton.xml",mimeType="application/octet-stream")]    
        private static const CYBORG_SKELETON:Class;    
        
        private var factory:StarlingFactory;    
        private var armature:Armature;    
        private var rightHand:Bone;    
        private var leftHand:Bone;    
        
        private var weaponID:int = 1;    
        
        private var canShoot:Boolean = true;    
        
        private var shootTimer:Timer;    
        
        public function GameState()    
        {    
            super();    
        }    
        
        override public function initialize():void    
        {    
            super.initialize();    
            
            StarlingArt.setLoopAnimations(["idle"]);    
            
            var nape:Nape = new Nape("nape");    
            nape.visible = true;    
            add(nape);    
            
            var floor:Platform = new Platform("floor", {width: 1400, height: 20});    
            floor.x = 700;    
            floor.y = stage.stageHeight;    
            add(floor);    
            
            for (var i:int = 1; i <= 2; i++)    
            {    
                var wall:Platform = new Platform("wall" + String(i), {width: 20, height: 400});    
                wall.y = 200;    
                
                if (i == 1)    
                {    
                    wall.x = 0;    
                }    
                else    
                {    
                    wall.x = 1400;    
                }    
                
                add(wall);    
            }    
            
            var atlas:TextureAtlas = new TextureAtlas(Texture.fromBitmap(new CYBORG_PNG()), XML(new CYBORG_TEX_DATA()));    
            
            factory = new StarlingFactory();    
            factory.addSkeletonData(XMLDataParser.parseSkeletonData(XML(new CYBORG_SKELETON())));    
            factory.addTextureAtlas(atlas, "CyborgCitrus");    
            
            armature = factory.buildArmature("cyborg");    
            
            rightHand = armature.getBone("armOutside");    
            leftHand = armature.getBone("armInside");    
            
            var hero:Hero = new Hero("hero", {registration: "topLeft", width: 70, height: 200});    
            hero.view = armature;    
            hero.x = 700;    
            hero.y = 100;    
            add(hero);    
            
            view.camera.setUp(hero, new Rectangle(0, 0, 1400, 400), new Point(.5, .5));    
            
            _ce.input.keyboard.addKeyAction("ChangeWeapon", Keyboard.A);    
            _ce.input.keyboard.addKeyAction("Shoot", Keyboard.S);    
            
            shootTimer = new Timer(300, 0);    
            shootTimer.addEventListener(TimerEvent.TIMER, shootHandler);    
        }    
        
        override public function update(timeDelta:Number):void    
        {    
            super.update(timeDelta);    
            
            if (_ce.input.isDoing("Shoot") && canShoot)    
            {    
                rightHand.childArmature.animation.gotoAndPlay("weapon" + weaponID + "shoot", .1);    
                leftHand.childArmature.animation.gotoAndPlay("weapon" + weaponID + "shoot", .1);    
                
                rightHand.childArmature.addEventListener(AnimationEvent.COMPLETE, animComplete, false, 0, true);    
                leftHand.childArmature.addEventListener(AnimationEvent.COMPLETE, animComplete, false, 0, true);    
                
                canShoot = false;    
                shootTimer.start();    
            }    
            
            if (_ce.input.justDid("ChangeWeapon"))    
            {    
                changeWeapon();    
            }    
        }    
        
        private function shootHandler(event:TimerEvent):void    
        {    
            canShoot = true;    
            
            shootTimer.stop();    
        }    
        
        private function changeWeapon():void    
        {    
            if (weaponID < 4)    
            {    
                weaponID++;    
            }    
            else    
            {    
                weaponID = 1;    
            }    
           
            rightHand.childArmature.animation.gotoAndPlay("weapon" + weaponID);    
            leftHand.childArmature.animation.gotoAndPlay("weapon" + weaponID);    
            
            switch (weaponID)    
            {    
                case 1:     
                    shootTimer.delay = 500;    
                    break;    
                
                case 2:     
                case 4:     
                    shootTimer.delay = 1000;    
                    break;    
                
                case 3:     
                    shootTimer.delay = 300;    
                    break;    
            }    
        }    
        
        private function animComplete(event:AnimationEvent):void    
        {    
            rightHand.childArmature.animation.gotoAndPlay("weapon" + weaponID, .1);    
            leftHand.childArmature.animation.gotoAndPlay("weapon" + weaponID, .1);    
            
            rightHand.childArmature.removeEventListener(AnimationEvent.COMPLETE, animComplete);    
            leftHand.childArmature.removeEventListener(AnimationEvent.COMPLETE, animComplete);    
        }    
    }    
}


Variables declaration
23-30: Embed the necessary DB files. _PNG & _TEX_DATA for creating the Texture Atlas, and _SKELETON for the DB animation.
32: factory variable to build the DB armature.
33. armature variable to store reference to the created armature
34-35: Variables to store reference to the left hand and right hand bones.

37: weaponID. Is information which weapon is currently active. From 1 to 4. Based on the both hands frame label: weapon1, weapon2, and so on.
39-41: Needed to create shooting behavior.

Initialize() Method
52: Add the ‘idle’ animation to become loop-able. Otherwise, it won’t loop.
58-78: Create the level. Well, just a floor and walls.
80: Create the Texture Atlas using the embedded image and XML.
82-84: Initialize the factory object. Add previously created Texture Atlas. Also the skeleton data.
86-89: Initialize the armature, leftHand, and rightHand object.

91-95: Create a Hero, and pass the armature object to its view property.
97: Set up camera to follow the hero

99-100: Add two new keyboard functions. Keyboard A button for switching weapon, and the S button for shooting.

102-103: Initialize the shoot timer.

Update() method
Override the update() method, and add the codes to check the pressed button.

112-114: If S button is pressed and the value for canFire variable is true, then for both of the hands, move to its shoot animation frames. If the current weapon is machine gun which is labeled as weapon1, then it will play weapon1shoot frame. And so on.
From the hand bones, access its childArmature, then access the animation property, and finally move to the target frame using gotoAndPlay() method.
Set the fadeInTime parameter to 0.1 so the animation blending is not too long.

115-116: Add Complete event for both hands. So if the shoot animation for weapon1shoot is completed, it will jump to weapon1 frame.
The event is defined on animComplete() method at line 166. And in this method the event listener for both hands also will be removed.

118-119: Set the canFire value to false and start the timer. If the timer is completed, it will fire the shootHandler() method that will reset the canFire value to true and stop the timer. So you can achieve a shooting delay, or a fire rate effect.

122: If the A button is pressed it will execute changeWeapon() method.

Switching Weapon
135-144: If changeWeapon() method is fired, then the first thing to do is to change the weaponID, in this case it just increase it.
If the value of weaponID is more than 4, then revert back to 1.

146-147: Then simply change the current frame to the next weapon frame, using that weaponID for sure.
The process is similar like how we change to shooting animation before.

149-163: Just some cosmetic. Change the shootTimer delay or fire rate according to the weapon type. Machine gun at 500 ms, sniper and rocket launcher at 1 second, etc.

Done
Compile and see the result.


Arrow keys to move the character, Spacebar to jump, A key to change weapon, and S key to shoot.

Alright, the tutorial is done.
Now you can achieve a nested animation and also weapon switching feature for your game, just like the old Flash movieclip, with gotoAndPlay and so on.
Well, not as straight forward than movieclip, but still, this is a great feature from Dragon Bones.

And finally, the source code.

Source Code
Download source here. It’s FlashDevelop project. CE & Nape .swc file included. As well as the .fla file contains the DB animation.
Download Source Code

Freelance 2D game artist, occassional game developers, lazy blogger, and professional procrasctinator

2 comments:

  1. mantaps, thanks :)

    ReplyDelete
  2. Thanks for this post. I would really love to do my own game as well. I'm currently looking at unblocked gamers of new updated games.

    ReplyDelete