Create objects and Art in the Citrus Engine

Update : if you want a version with a SWC for targeting iOS, please refers to this post.

Hey ! I started to make my school game project this week. I advance quickly, it should be fine for the end of june !

For this third tutorial on the Citrus Engine, I explain better how you can use the Flash IDE as Level Editor, and I show two classes that I’ve created for my game. Here is what you will have.

Same as the last tutorial, create a fla and your MovieClips with params. This is my level :
This is my level
Adding platforms is always a difficult task, because of the rectangle’s angle. Try to don’t have corner ! A box2D platform with a rounded edge would be nice !

You can see the box2D objects on the game by opening the console with the TAB key, and write : set Box2D visible true

You should make several layers to handle the depth of field ! You can use a guide with your photoshop level to see it and set your layers. When you export your SWF, the photoshop layer will not be added. Your swf stay light 😉 This is my layers

All my decorations are on several layers. Background, Bg Elements and Grass are big PNG 1409 * 637. I put them on the top left corner. If the length of the level is higher than 4000, you can use negative coordinates. So your level max length will be approximately 7500 px !

We have already seen how you can add a view and params in your MovieClip :

var className = "objects.Roseau";
var params = {
	view: "objects/Roseau.swf"
}
 
var className = "objects.MusicalSensor"
var params = {
	song: "Si"
}

I created 4 MusicalSensor’s MovieClip, because I have 4 songs, I create one MovieClip by song. This is two new objects that I’ve created for my game. I will show their classes later.

Here is my Class Document :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package {
 
	import com.citrusengine.core.CitrusEngine;
 
	import flash.display.Loader;
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.net.URLRequest;
	import flash.text.TextField;
 
	/**
	 * @author Aymeric
	 */
	public class Main extends CitrusEngine {
 
		public var loading:TextField;
 
		public function Main() {
 
			super();
 
			sound.addSound("Si", "sounds/si.mp3");
			sound.addSound("Do", "sounds/do.mp3");
			sound.addSound("Re", "sounds/re.mp3");
			sound.addSound("Mi", "sounds/mi.mp3");
 
			var loader:Loader = new Loader();
			loader.load(new URLRequest("levels/LevelA1.swf"));
			loader.contentLoaderInfo.addEventListener(Event.COMPLETE, handleSWFLoadComplete);
		}
 
		private function handleSWFLoadComplete(evt:Event):void {
 
			var levelObjectsMC:MovieClip = evt.target.loader.content;
			state = new GameState(levelObjectsMC, loading);
 
			evt.target.removeEventListener(Event.COMPLETE, handleSWFLoadComplete);
			evt.target.loader.unloadAndStop();
		}
	}
}

The loading TextField come from the stage (not the level stage). I add 4 sounds that I use later.

The game state :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package {
 
	import Box2DAS.Dynamics.ContactEvent;
 
	import com.citrusengine.core.CitrusEngine;
	import com.citrusengine.core.CitrusObject;
	import com.citrusengine.core.State;
	import com.citrusengine.math.MathVector;
	import com.citrusengine.objects.CitrusSprite;
	import com.citrusengine.objects.platformer.Hero;
	import com.citrusengine.objects.platformer.Platform;
	import com.citrusengine.objects.platformer.Sensor;
	import com.citrusengine.physics.Box2D;
	import com.citrusengine.utils.ObjectMaker;
 
	import objects.MusicalSensor;
	import objects.Roseau;
 
	import flash.display.MovieClip;
	import flash.geom.Rectangle;
	import flash.text.TextField;
 
	/**
	 * @author Aymeric
	 */
	public class GameState extends State {
 
		private var _ce:CitrusEngine;
		private var _levelObjectsMC:MovieClip;
		private var _loading:TextField;
 
		public function GameState(levelObjectsMC:MovieClip, loading:TextField) {
 
			super();
 
			_ce = CitrusEngine.getInstance();
 
			_levelObjectsMC = levelObjectsMC;
			_loading = loading;
 
			var objects:Array = [Platform, Hero, CitrusSprite, Sensor, MusicalSensor, Roseau];
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			var box2D:Box2D = new Box2D("Box2D");
			add(box2D);
 
			view.loadManager.onLoadComplete.addOnce(handleLoadComplete);
 
			ObjectMaker.FromMovieClip(_levelObjectsMC);
 
			var hero:Hero = Hero(getObjectByName("Hero"));
 
			view.setupCamera(hero, new MathVector(320, 240), new Rectangle(0, 0, 1409, 637), new MathVector(.25, .05));
 
			var roseaux:Vector.<CitrusObject> = getObjectsByType(Roseau);
			for each (var roseau:Roseau in roseaux) {
				roseau.onBeginContact.add(_roseauTouche);
				roseau.onEndContact.add(_roseauFin);
			}
 
			var musicalSensors:Vector.<CitrusObject> = getObjectsByType(MusicalSensor);
			for each (var musicalSensor:MusicalSensor in musicalSensors) {
				musicalSensor.onBeginContact.add(_playSound);
			}
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			var loaded:int = Math.round(view.loadManager.bytesLoaded / view.loadManager.bytesTotal * 100)
			if (loaded < 100)
				_loading.text = loaded.toString();
			else
				_loading.text = "";
		}
 
		private function handleLoadComplete():void {
			//remove loader
		}
 
		private function _roseauTouche(cEvt:ContactEvent):void {
 
			if (cEvt.other.GetBody().GetUserData() is Hero) {
				cEvt.fixture.GetBody().GetUserData().anim = "white";
			}
		}
 
		private function _roseauFin(cEvt:ContactEvent):void {
 
			if (cEvt.other.GetBody().GetUserData() is Hero) {
				cEvt.fixture.GetBody().GetUserData().anim = "black";
			}
		}
 
		private function _playSound(cEvt:ContactEvent):void {
 
			if (cEvt.other.GetBody().GetUserData() is Hero) {
				_ce.sound.playSound(cEvt.fixture.GetBody().GetUserData().song, 1, 0);
			}
		}
 
	}
}

Roseaux are reeds (it’s French 🙂 ), when my hero touches them the light change into white, otherwise it’s black. My roseau is a swf with three labels : idle (black), white (black to white) and black (white to black). To change my label into the Citrus Engine, we may use myRoseau.gotoAndPlay(“white”); but it doesn’t work ! Indeed, Citrus objects are always updated in an enter frame so it “stays” on the same frame ! To do that, we use the animation property of Citrus’ objects :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package objects {
 
	import com.citrusengine.objects.platformer.Sensor;
 
	/**
	 * @author Aymeric
	 */
	public class Roseau extends Sensor {
 
		private var _anim:String = "idle";
 
		public function Roseau(name:String, params:Object = null) {
			super(name, params);
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			updateAnimation();
		}
 
		private function updateAnimation():void {
 
			_animation = _anim;
		}
 
		public function get anim():String {
			return _anim;
		}
		public function set anim(value:String):void {
			_anim = value;
		}
 
	}
}

My roseau extends Sensor because of the contact with the hero ! And I use it easily :

private function _roseauTouche(cEvt:ContactEvent):void {
 
	if (cEvt.other.GetBody().GetUserData() is Hero) {
		cEvt.fixture.GetBody().GetUserData().anim = "white";
	}
}
 
private function _roseauFin(cEvt:ContactEvent):void {
 
	if (cEvt.other.GetBody().GetUserData() is Hero) {
		cEvt.fixture.GetBody().GetUserData().anim = "black";
	}
}

Now the musical sensor, it is really a simple sensor, I just added a param song to play :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package objects {
 
	import com.citrusengine.objects.platformer.Sensor;
 
	/**
	 * @author Aymeric
	 */
	public class MusicalSensor extends Sensor {
 
		public var song:String;
 
		public function MusicalSensor(name:String, params:Object = null) {
			super(name, params);
		}
	}
}

And I use it like that :

private function _playSound(cEvt:ContactEvent):void {
 
	if (cEvt.other.GetBody().GetUserData() is Hero) {
		_ce.sound.playSound(cEvt.fixture.GetBody().GetUserData().song, 1, 0);
	}
}

Instead of cutting my flowers into MovieClip, I just put my musical sensor in front of them. It’s a bit lazy, but it is faster to do 🙂

You can download my zip.

21 thoughts on “Create objects and Art in the Citrus Engine

  1. Salut eremic ,
    Avant tout merci pour ces tutos , je comprend pas comment tu as importé les fichiers as de citrus engine dans l’environnement flash professional.
    Je suis Graphiste sa me serais très utile. Merci

  2. Thanks for the tutorial! Very nice job!

    Where can I grab the .fla file of the character? 🙂

  3. Hi Aymeric and thanks for the valuable posts, i intend to use CE to develop a platform game for iOS with Flex 4.5 and was wondering if there is a way to overcome the issue of not being able to load swfs at run time for iOS? Any advice will be appreciated- thanks.

  4. Thanks for the tip, for example, in the CE demo files, would this also work with embedding the whole level/layout.swf and then having ObjectMaker handle creation of the layout.swf objects or do i need to work with the layout objects as Symbols?

  5. I juste made a post about embed assets. I think there is always a lot of works to do from that. I’ve to handle SWC, it will be easier I think.

    You need to do performance test if you are targeting mobile devices. Box2D alchemy is really greedy. It depends of your game design obviously.

    About, your second comment, I will send you an email 😉

  6. Hi Aymeric,

    The Roseau item overrides the update function and adds an update animation
    which changes _animation to anim which can be set externally. This gets called at every update step. Is this a heavy way to go? seems that the animation only needs to be updated if a contact event had occurred?

  7. Hey Paul,

    No, I don’t think that it is too heavy. You may optimized if you make the animation change into the class itself, not on the state.

    The Citrus Engine needs an enter frame function to play the MC animation fully.

  8. Ok got that. Just to clear my head though, here we are just waiting to switch from black to white on contact- maybe its even just a static image – but even if it isn’t, would the animation(eg black.swf) not just continue to play without further calls until an endContact would switch it back to black?

    Thanks for taking time to teach and for your help

    Best Paul

  9. Yes the function will always be called, but the animation will stop because it is a gotoAndStop in my swf! The Citrus Engine “manage” the animation loop or not.

    Even if the animate function is called, there isn’t a performance drop, it is not significative.
    Maybe if you have more than 100 objects like that, you will optimize 😉

  10. Awesome tutorial you got here! And awesome job on your Level Manager. I understand it thoroughly though I keep on getting bugs like

    TypeError: Error #1009: Cannot access a property or method of a null object reference.
    at GameState/initialize()[C:\Users\Elaine\Documents\ITRP1\CEGame\src\GameState.as:88]
    at com.citrusengine.core::CitrusEngine/handleEnterFrame()[C:\Users\Elaine\Documents\New folder (7)\ce2\ce2\Source\com\citrusengine\core\CitrusEngine.as:159]

  11. I know, that this is a dumb question, but I have a few problems right now. Is it right, that this code (in the gamestate-class) should create a white instance of roseau?

    var roseau:Roseau;
    roseau= new Roseau(“roseau”, {view:”art/Roseau.swf”, width: 100, height: 100, x: gx, y: gy});
    add(roseau);
    roseau.body.GetUserData().anim = “white”;

    I ask, because it is always black …

  12. Hey, I’ve no problem. I’ve updated the code like that :

    var roseaux:Vector. = getObjectsByType(Roseau);
    for each (var roseau:Roseau in roseaux) {
    roseau.onBeginContact.add(_roseauTouche);
    roseau.onEndContact.add(_roseauFin);
    roseau.anim = “white”;
    }

    And my roseau are white. Try roseau.anim = “white” 😉

  13. After a little break, I think I finally got the sollution. I think the reason is, that my roseau are loaded at runtime. I try to access the timeline, before the swf is completely loaded. But how can I check if its loaded? Is there a way to add an EventListener or something to a CitrusObject? SpriteArt has a ContentLoaded-Handler, but I dont want to modify this class.

Leave a Reply

Your email address will not be published. Required fields are marked *