Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


9. December 2013

 

In the second part of the LibGDX Scene2D tutorial we will now look at Actions.  Actions are a convenient ( and completely optional! ) way to get your game’s Actors to “do stuff”.

 

Let’s jump straight in to an example:

package com.gamefromscratch;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.MoveToAction;


public class SceneDemo3 implements ApplicationListener {
    
    public class MyActor extends Actor {
        Texture texture = new Texture(Gdx.files.internal("data/jet.png"));
        public boolean started = false;

        public MyActor(){
            setBounds(getX(),getY(),texture.getWidth(),texture.getHeight());
        }
        
        @Override
        public void draw(Batch batch, float alpha){
            batch.draw(texture,this.getX(),getY());
        }
    }
    private Stage stage;
    
    @Override
    public void create() {        
        stage = new Stage();
        Gdx.input.setInputProcessor(stage);
        
        MyActor myActor = new MyActor();
        
        MoveToAction moveAction = new MoveToAction();
        moveAction.setPosition(300f, 0f);
        moveAction.setDuration(10f);
        myActor.addAction(moveAction);
        
        stage.addActor(myActor);
    }

    @Override
    public void dispose() {
    }

    @Override
    public void render() {    
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        stage.act(Gdx.graphics.getDeltaTime());
        stage.draw();
    }

    @Override
    public void resize(int width, int height) {
    }

    @Override
    public void pause() {
    }

    @Override
    public void resume() {
    }
}

 

When you run this example, it will slowly move the jet until it is at the position 300,0.

MoveToActionReduced

 

Much of this code is recycled from the previous Scene2D example, so we will only discuss the new portions, which reside in the create() function:

@Override
public void create() {        
    stage = new Stage();
    Gdx.input.setInputProcessor(stage);
    
    MyActor myActor = new MyActor();
    
    MoveToAction moveAction = new MoveToAction();
    moveAction.setPosition(300f, 0f);
    moveAction.setDuration(10f);
    myActor.addAction(moveAction);
    
    stage.addActor(myActor);
}

Here we create a MoveToAction, which moves the attached Actor to a given position over time.  You set the position to move to with the method setPosition() and the total duration of the action using setDuration().  You assign the action to the Actor using addAction().  All actions are derived from the Action class, while MoveToAction is derived from TemporalAction, which is an action that has a duration.  In addition to MoveToAction, there are a number of others such as MoveByAction, ScaleToAction, ColorAction, DelayAction, RepeatAction, RotateByAction, etc.

 

There is one important thing to be aware of here.  Back in part one of the Scene2D tutorial our custom Actor MyActor overrode the act method.  It is the act() method of Actor that updates all of the Actions connected to an Actor.  Therefore if you create your own Actor class, be sure to call it’s parent’s act() or call each attached Action manually like so:

@Override
public void act(float delta){
    for(Iterator<Action> iter = this.getActions().iterator(); iter.hasNext();){
        iter.next().act(delta);
    }
}

If you run more than one actions like so:

MoveToAction moveAction = new MoveToAction();
RotateToAction rotateAction = new RotateToAction();
ScaleToAction scaleAction = new ScaleToAction(); 

moveAction.setPosition(300f, 0f);
moveAction.setDuration(5f);
rotateAction.setRotation(90f);
rotateAction.setDuration(5f);
scaleAction.setScale(0.5f);
scaleAction.setDuration(5f);

myActor.addAction(moveAction);
myActor.addAction(rotateAction);
myActor.addAction(scaleAction);

stage.addActor(myActor);

 

You will see:

RotateScaleMoveActionReduced

As you can see, all of your actions run concurrently by default.

 

There is one important thing to be aware of.  The draw() method in MyActor we’ve been using until this point is not capable of displaying rotation or scaling by default.  If you wish to enabled scaled/rotated drawing like above, you need to make a minor adjustment to draw(), like so:

 

@Override
public void draw(Batch batch, float alpha){
    batch.draw(texture,this.getX(),getY(),this.getOriginX(),this.getOriginY(),this.getWidth(),
            this.getHeight(),this.getScaleX(), this.getScaleY(),this.getRotation(),0,0,
            texture.getWidth(),texture.getHeight(),false,false);
}

 

Obviously there are going to be many times when you want to delay an action, or run them in sequence.  Fortunately LibGdx supports exactly this.  Say you wanted to scale then rotate then move your Actor, you can accomplish this using a SequenceAction like so:

 

final MyActor myActor = new MyActor();

SequenceAction sequenceAction = new SequenceAction();

MoveToAction moveAction = new MoveToAction();
RotateToAction rotateAction = new RotateToAction();
ScaleToAction scaleAction = new ScaleToAction(); 

moveAction.setPosition(300f, 0f);
moveAction.setDuration(5f);
rotateAction.setRotation(90f);
rotateAction.setDuration(5f);
scaleAction.setScale(0.5f);
scaleAction.setDuration(5f);

sequenceAction.addAction(scaleAction);
sequenceAction.addAction(rotateAction);
sequenceAction.addAction(moveAction);


myActor.addAction(sequenceAction);

stage.addActor(myActor);

 

Simply add all the actors to the SequenceAction, then one will run, followed by the next then the next.

By statically importing Actions, like so:

import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*;

You can also chain actions, so it’s possible to express the above action like:

myActor.addAction(sequence(scaleTo(0.5f,0.5f,5f),rotateTo(90.0f,5f),moveTo(300.0f,0f,5f)));
stage.addActor(myActor);

 

Or run in parallel, like so:

myActor.addAction(parallel(scaleTo(0.5f,0.5f,5f),rotateTo(90.0f,5f),moveTo(300.0f,0f,5f)));
stage.addActor(myActor);

There are several other actions, such as Int or FloatAction, that allow you to modify a value over time, DelayAction for running an action, um, after a delay, RepeatAction, for performing an Action over and over.  However, they all follow the same basic layout, so you should be able to figure them out easily at this point.  One last thing to point out, all Actions are poolable.  This means you can keep a pool of actions instead of allocating them over and over.  Here is one such example of a Pool of MoveToActions:

final MyActor myActor = new MyActor();
Pool<MoveToAction> actionPool = new Pool<MoveToAction>(){
    protected MoveToAction newObject(){
        return new MoveToAction();
    }
};

MoveToAction moveAction = actionPool.obtain();
moveAction.setDuration(5f);
moveAction.setPosition(300f, 0);

myActor.addAction(moveAction);

stage.addActor(myActor);

 

You can also have an action simply be code.  Consider the above example, let’s say you wanted to log when the MoveToAction was complete.  You could accomplish this by doing the following:

myActor.addAction(sequence(moveAction,
        run(new Runnable(){
            @Override
            public void run() {
                Gdx.app.log("STATUS", "Action complete");
            }
            
})));

 

A Runnable action simply runs the code specified in the run method when, um, run. That’s about it for Actions.  On their own they are pretty simply, but can easily be chained to create very complex interactions.

Programming , ,

blog comments powered by Disqus

Month List

Popular Comments

Cocos2D HTML–Porting to the most recent version
Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


10. August 2012

 

I received a comment from a Cocos2D-HTML developer saying that the API is complete for the upcoming beta release, so that I could update the tutorials on this site.  So today I jumped in to see exactly what changed and the answer is quite a bit!  In the process of updating the code to work with the newest Cocos2D code, I went through a great deal of pain tracking down those changes.  This post is to illustrate what has changed, so you don’t have to go through the same process.

 

The code being ported is the example code from this tutorial.

Changes:

 

Filename changes

 

I encountered a few filename changes ( which JavaScript can make exceedingly annoying to find ).  First off, a few files have had their casing changed, which you won’t notice you are working in Windows, but under Linux you will.  Mostly it’s a matter of files being renamed from cc to CC.  Additionally the folder keypad_dispatcher is now keyboard_dispacter, and CCKeypadDelegate.js and CCKeypadDispatcher.cs have been renamed to CCKeyboardDelegate.js and CCKeyboardDispatcher.cs respectively.

 

Function changes

 

ccp is now Point

ccc2/3/4/ is now Color2B,Color3B,Color4B

Layer.setIsRelativeAnchorPoint has been removed.

Director.sharedDirector() is now Director.getInstance();

setIsTouchEnabled() is simply isTouchEnabled()

setIsKeypadEnabled() is now isKeyboardEnabled()

 

Events have been renamed, for example:

ccTouchesEnded is now onTouchesEnded

 

The Keyboard delegate have renamed events from:

keyDown and keyUp to onKeyDown and onKeyUp

cc.key.[name] is now cc.KEY.[name]

cc.Touch ( the value passed to touch handlers ) has changed from .locationInView() to .getLocation()

cc.AudioManager.sharedEngine() is now cc.AudioEngine.getInstance();

cc.Log is now cc.log

 

Bootstrap change

 

This is where the biggest changes occurred.  The startup process is completely different.  First of all, library loading is no longer the responsibility of the  application, it is instead handled by jsloader.js.

 

This changes cocos2d.js massively:

cocos2d.js

(function () {
    var d = document;
    var c = {
        COCOS2D_DEBUG:2, //0 to turn debug off, 1 for basic debug, and 2 for full debug
        box2d:false,
        showFPS:true,
        frameRate:60,
        tag:'gameCanvas', //the dom element to run cocos2d on
        engineDir:'../cocos2d/',
        appFiles:['MyFirstApp.js']
    };
    window.addEventListener('DOMContentLoaded', function () {
        //first load engine file if specified
        var s = d.createElement('script');
        s.src = c.engineDir + 'platform/jsloader.js';
        d.body.appendChild(s);
        s.c = c;
        s.id = 'cocos2d-html5';
        //else if single file specified, load singlefile
    });
})();

If you haven’t seen this syntax before, it is basically an anonymous function that will execute at the end of declaration ( because of the () it ends in ).  The majority of what is going on here is the creation of a configuration file ‘c’, where a number of settings are, um, set.  tag is the name of the canvas element in your html file, engineDir is where you installed cocos2D to, appFiles are additional files you write ( in addition to your main.js ).  Basically every file you create needs to be added to this array.  It then adds an event that will be fired once the DOM has been populated, which will then create a new <SCRIPT> tag in your HTML document, then pass in the path to the jsloader, which will then load all of the cocos2D libraries, as well as anything your specified in the appFiles array.  Finally it stores the configuration settings in ‘c’ in the newly created script tag.

 

In a change I disagree with, the framework now forces you to add a file named main.js, which will be called by jsloader at the end of the loading process.  IMHO, this should be added as part of the configuration you set above.  This file replaced AppDelegate.js from the tutorials.  Let’s take a look at the new main.js:

var cocos2dApp = cc.Application.extend({
    config:document.querySelector('#cocos2d-html5')['c'],
    ctor:function (scene) {
        this._super();
        this.startScene = scene;
        cc.COCOS2D_DEBUG = this.config['COCOS2D_DEBUG'];
        cc.setup(this.config['tag']);
        cc.Loader.shareLoader().onloading = function () {
            cc.LoaderScene.shareLoaderScene().draw();
        };
        cc.Loader.shareLoader().onload = function () {
            cc.AppController.shareAppController().didFinishLaunchingWithOptions();
        };
        cc.Loader.shareLoader().preload([
        ]);
    },
    applicationDidFinishLaunching:function () {
        // initialize director
        var director = cc.Director.getInstance();
        director.setDisplayStats(this.config['showFPS']);
        director.setAnimationInterval(1.0 / this.config['frameRate']);
        director.runWithScene(new this.startScene());

        return true;
    }
});
var myApp = new cocos2dApp(MyFirstAppScene);

Things have changed a fair bit here, but the process is pretty much the same.  First thing to notice is it pulls the configuration data ‘c’ and stores it in the variable config, which is then used to setup cocos2D.  The next major thing to notice is we now create an instance of our app, and pass in the scene to start with. 

 

Now let’s take a look at the changes to MyFirstApp.cs:

var MyFirstApp = cc.Layer.extend({
    init:function()
    {
        this._super();

        var s = cc.Director.getInstance().getWinSize();

        var layer1 = cc.LayerColor.create(new cc.Color4B(255, 255, 0, 255), 600, 600);
        //layer1.setPosition(new cc.Point(s.width/2,s.height/2));
        //layer1.setIsRelativeAnchorPoint(true);
        layer1.setAnchorPoint(new cc.Point(0.5,0.5));

        
        var helloLabel = cc.LabelTTF.create("Hello world", "Arial", 30);
        helloLabel.setPosition(new cc.Point(s.width/2,s.height/2));
        helloLabel.setColor(new cc.Color3B(255,0,0));
        var rotationAmount = 0;
        var scale = 1;
        helloLabel.schedule(function()
            {
                this.setRotation(rotationAmount++);
                if(rotationAmount > 360)
                    rotationAmount = 0;
                this.setScale(scale);
                scale+= 0.05;
                if(scale > 10)
                    scale =1;
            });

        layer1.addChild(helloLabel);
        this.addChild(layer1);

        
        return true;
    }

});

var MyFirstAppScene = cc.Scene.extend({
    onEnter:function(){
        this._super();
        var layer = new MyFirstApp();
        layer.init();
        this.addChild(layer);
    }
})

 

Things have changed a bit here.  First we now extend a scene to create a new object MyFirstAppScene.  This in turn creates a layer and adds it to itself.  The onEnter event is called when the scene is activated ( via RunWithScene ).  Otherwise all of our changes are those described above ( function renames, removal, etc… ).

 

 

I hope that helps you port your existing code to the newest codebase.  All of the changes are improvements ( except perhaps the whole forced main.js thing, I still don’t like that ), so it is worth the pain.  I hope to get the tutorials all updated shortly, but unfortunately, these changes more or less will result in complete re-writes of 3 tutorials, so it might take a bit! Sad smile

Programming , ,

blog comments powered by Disqus

Month List

Popular Comments