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”