Create a game like Osmos

2 days ago, I’ve offered a new beta for the Citrus Engine. I had really good feedbacks, thanks guys for having regard on the engine!
This new beta also introduced a new demo which was again a platfomer game. It’s time to show, thanks to a quick case study, that the Citrus Engine is not only made for platformer games! For this first example, we will create a game like Osmos. I really love this game, it’s zen, gameplay mechanics are easy and powerful, definitely a good indie game.

In 4 hours of work, this is what I made. You can drag & drop atoms.
Source are available on the CE’s GitHub, in the src/games package.

Osmos is mainly based on collision detection & management. If your atom is bigger than the other one, you will increase, if it is smaller decrease. I don’t know how collision detection are perfomed but it may use a physics engine like Box2D.
In the Citrus Engine you have four options for managing logics/physics : Box2D, Nape, the simple math based class, or create your own. The simple math based doesn’t detect collision with circle, so forget about it and I’m to lazy to create a new collision management system for this small demo 😛 So we will use a physics engine.
Box2D or Nape? Both do the job. However in Osmos, the cells increase/decrease all the time, this is the basic behavior. You’ve to know that with Box2D you can’t scale a body after its creation, you have to destroy/recreate it whereas you can with Nape. No more hesitation we will use Nape, moreover it is easier to handle.

Concerning graphics, no doubt, I used the flash graphics API. We could made it with Starling, but it is more complicated to create a circle. In the future if you want to use image & texture you will be able to switch easily to Starling thanks to the Citrus Engine power.

Game logic : after a collision we will increase or decrease the physics shape and update its view. It’s easy to made in the CE :

myAtom.view.changeSize(newDiameter);

Also we have to remove the physics collision, atoms will overlap (and their size changed of course) but aren’t be throw away!

Let’s create some code. Our main file :

package {
 
	import games.osmos.OsmosGameState;
 
	import com.citrusengine.core.CitrusEngine;
 
	[SWF(frameRate="60")]
 
	/**
	 * @author Aymeric
	 */
	public class Main extends CitrusEngine {
 
		public function Main() {
 
			// import libraries from the libs folder, select just one Nape swc.
 
			state = new OsmosGameState();
		}
	}
}

The GameState :

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
package games.osmos {
 
	import nape.geom.Vec2;
 
	import com.citrusengine.core.State;
	import com.citrusengine.objects.platformer.nape.Platform;
	import com.citrusengine.physics.Nape;
 
	import flash.display.DisplayObject;
	import flash.events.MouseEvent;
 
	/**
	 * @author Aymeric
	 */
	public class OsmosGameState extends State {
 
		private var _clickedAtom:Atom;
 
		public function OsmosGameState() {
			super();
		}
 
		override public function initialize():void {
 
			super.initialize();
 
			var nape:Nape = new Nape("nape", {gravity:new Vec2()});
			//nape.visible = true;
			add(nape);
 
			add(new Platform("platformTop", {x:0, y:-10, width:stage.stageWidth, height:10}));
			add(new Platform("platformRight", {x:stage.stageWidth, y:0, width:10, height:stage.stageHeight}));
			add(new Platform("platformBot", {x:0, y:stage.stageHeight, width:stage.stageWidth, height:10}));
			add(new Platform("platformLeft", {x:-10, y:0, width:10, height:stage.stageHeight}));
 
			var atom:Atom, radius:Number;
 
			for (var i:uint = 0; i < 10; ++i) {
 
				for (var j:uint = 0; j < 10; ++j) {
 
					radius = 3 + Math.random() * 30;
					atom = new Atom("atom"+i+j, {x:i * 50, y:j * 50, radius:radius, view:new AtomArt(radius), registration:"topLeft"});
					add(atom);
					(view.getArt(atom) as DisplayObject).addEventListener(MouseEvent.MOUSE_DOWN, _handleGrab);
				}
			}
 
			stage.addEventListener(MouseEvent.MOUSE_UP, _handleRelease);
		}
 
		private function _handleGrab(mEvt:MouseEvent):void {
 
			_clickedAtom = view.getObjectFromArt(mEvt.currentTarget) as Atom;
 
			if (_clickedAtom)
				_clickedAtom.enableHolding(mEvt.currentTarget.parent);
		}
 
		private function _handleRelease(mEvt:MouseEvent):void {
			_clickedAtom.disableHolding();
		}
	}
}

We removed gravity, create game limits and add many atoms. Each one can be drag/dropped!

The AtomArt class :

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
package games.osmos {
 
	import flash.display.Sprite;
 
	/**
	 * @author Aymeric
	 */
	public class AtomArt extends Sprite {
 
		private var _color:uint;
 
		public function AtomArt(radius:Number) {
 
			_color = Math.random() * 0xFFFFFF;
 
			this.graphics.beginFill(_color);
			this.graphics.drawCircle(0, 0, radius);
			this.graphics.endFill();
		}
 
		public function changeSize(diameter:Number):void {
 
			this.graphics.clear();
 
			if (diameter > 0) {
				this.graphics.beginFill(_color);
				this.graphics.drawCircle(0, 0, diameter * 0.5);
				this.graphics.endFill();
			}
		} 
	}
}

The update will be called by the physics part.
The Atom class :

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package games.osmos {
 
	import nape.callbacks.CbEvent;
	import nape.callbacks.CbType;
	import nape.callbacks.InteractionCallback;
	import nape.callbacks.InteractionListener;
	import nape.callbacks.InteractionType;
	import nape.callbacks.PreCallback;
	import nape.callbacks.PreFlag;
	import nape.callbacks.PreListener;
	import nape.constraint.PivotJoint;
	import nape.geom.Vec2;
	import nape.phys.Body;
	import nape.phys.BodyList;
 
	import com.citrusengine.objects.NapePhysicsObject;
 
	import flash.display.DisplayObject;
 
	/**
	 * @author Aymeric
	 */
	public class Atom extends NapePhysicsObject {
 
		public static const ATOM:CbType = new CbType();
 
		public var size:String = "";
 
		private var _preListener:PreListener;
 
		private var _hand:PivotJoint;
		private var _mouseScope:DisplayObject;
 
		public function Atom(name:String, params:Object = null) {
 
			super(name, params);
		}
 
		override protected function createConstraint():void {
 
			super.createConstraint();
 
			// we need to ignore physics collision
			_preListener = new PreListener(InteractionType.ANY, ATOM, ATOM, handlePreContact);
			_body.space.listeners.add(_preListener);
			_body.cbTypes.add(ATOM);
 
			_nape.space.listeners.add(new InteractionListener(CbEvent.ONGOING, InteractionType.ANY, ATOM, ATOM, handleOnGoingContact));
 
			_hand = new PivotJoint(_nape.space.world, null, new Vec2(), new Vec2());
			_hand.active = false;
			_hand.stiff = false;
			_hand.space = _nape.space;
			_hand.maxForce = 5;
		}
 
		override public function destroy():void {
 
			_hand.space = null;
 
			_preListener.space = null;
			_preListener = null;
 
			super.destroy();
		}
 
		override public function update(timeDelta:Number):void {
 
			super.update(timeDelta);
 
			if (_mouseScope)
				_hand.anchor1.setxy(_mouseScope.mouseX, _mouseScope.mouseY);
 
			var bodyDiameter:Number;
 
			if (size == "bigger") {
				bodyDiameter = _body.shapes.at(0).bounds.width;
				bodyDiameter > 100 ? _body.scaleShapes(1.003, 1.003) : _body.scaleShapes(1.01, 1.01); 
				bodyDiameter = _body.shapes.at(0).bounds.width;
				(view as AtomArt).changeSize(bodyDiameter);
 
			} else if (size == "smaller") {
				_body.scaleShapes(0.9, 0.9);
				bodyDiameter = _body.shapes.at(0).bounds.width;
				(view as AtomArt).changeSize(bodyDiameter);
 
				if (_body.shapes.at(0).bounds.width < 1)
					this.kill = true;
			}
 
			size = "";
		}
 
		public function enableHolding(mouseScope:DisplayObject):void {
 
			_mouseScope = mouseScope;
 
			var mp:Vec2 = new Vec2(mouseScope.mouseX, mouseScope.mouseY);
            var bodies:BodyList = _nape.space.bodiesUnderPoint(mp);
            for(var i:int = 0; i < bodies.length; ++i) {
                var b:Body = bodies.at(i);
                if(!b.isDynamic()) continue;
                _hand.body2 = b;
                _hand.anchor2 = b.worldToLocal(mp);
                _hand.active = true;
                break;
            }
 
		}
 
		public function disableHolding():void {
 
			_hand.active = false;
			_mouseScope = null;
		}
 
		protected function handleOnGoingContact(callback:InteractionCallback):void {
 
			var atom1:Atom = callback.int1.userData.myData as Atom;
			var atom2:Atom = callback.int2.userData.myData as Atom;
 
			if (atom1.body.shapes.at(0).bounds.width > atom2.body.shapes.at(0).bounds.width) {
				 atom1.size = "bigger";
				 atom2.size = "smaller";
			} else {
				atom1.size = "smaller";
				atom2.size = "bigger";
			}
		}
 
		override public function handlePreContact(callback:PreCallback):PreFlag {
 
			return PreFlag.IGNORE;
		}
 
	}
}

If the atom is too small, it is destroyed. Again take a look on Nape documentation if you have some trouble. It wasn’t the CE part, the difficult one, isn’t it?

And that’s it for this demo. Now you have to find the good parameters & balance to have the same user experience than Osmos… the hardest part!

That was a non platformer game tutorial for the CE 😀 I hope to be able to provide more soon! Which game would you like?

1 thought on “Create a game like Osmos

Leave a Reply

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