{"id":714,"date":"2012-08-23T15:01:58","date_gmt":"2012-08-23T14:01:58","guid":{"rendered":"http:\/\/www.aymericlamboley.fr\/blog\/?p=714"},"modified":"2014-11-01T14:52:07","modified_gmt":"2014-11-01T13:52:07","slug":"create-a-game-like-osmos","status":"publish","type":"post","link":"http:\/\/www.aymericlamboley.fr\/blog\/create-a-game-like-osmos\/","title":{"rendered":"Create a game like Osmos"},"content":{"rendered":"<p><a href=\"http:\/\/www.aymericlamboley.fr\/blog\/citrus-engine-v3-beta2\/\" target=\"_blank\">2 days ago<\/a>, I&#8217;ve offered a new beta for the <a href=\"http:\/\/citrusengine.com\/\" target=\"_blank\">Citrus Engine<\/a>. I had really good feedbacks, thanks guys for having regard on the engine!<br \/>\nThis new beta also introduced a new demo which was again a platfomer game. It&#8217;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 <a href=\"http:\/\/www.hemispheregames.com\/osmos\/\" target=\"_blank\">Osmos<\/a>. I really love this game, it&#8217;s zen, gameplay mechanics are easy and powerful, definitely a good indie game.<\/p>\n<p><a href=\"http:\/\/www.aymericlamboley.fr\/blog\/wp-content\/uploads\/2012\/08\/osmos.html\" target=\"_blank\"><em>In 4 hours of work, this is what I made<\/em><\/a>. You can drag &#038; drop atoms.<br \/>\nSource are available on the CE&#8217;s <a href=\"https:\/\/github.com\/alamboley\/Citrus-Engine\" target=\"_blank\">GitHub<\/a>, in the src\/games package.<\/p>\n<p><!--more--><\/p>\n<p>Osmos is mainly based on collision detection &#038; management. If your atom is bigger than the other one, you will increase, if it is smaller decrease. I don&#8217;t know how collision detection are perfomed but it may use a physics engine like Box2D.<br \/>\nIn 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&#8217;t detect collision with circle, so forget about it and I&#8217;m to lazy to create a new collision management system for this small demo \ud83d\ude1b So we will use a physics engine.<br \/>\nBox2D or Nape? Both do the job. However in Osmos, the cells increase\/decrease all the time, this is the basic behavior. You&#8217;ve to know that with Box2D you can&#8217;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.<\/p>\n<p>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 &#038; texture you will be able to switch easily to Starling thanks to the Citrus Engine power.<\/p>\n<p>Game logic : after a collision we will increase or decrease the physics shape and update its view. It&#8217;s easy to made in the CE :<\/p>\n<pre lang=\"actionscript3\">myAtom.view.changeSize(newDiameter);<\/pre>\n<p> Also we have to remove the physics collision, atoms will overlap (and their size changed of course) but aren&#8217;t be throw away!<\/p>\n<p>Let&#8217;s create some code. Our main file :<\/p>\n<pre lang=\"actionscript3\">package {\r\n\r\n\timport games.osmos.OsmosGameState;\r\n\r\n\timport com.citrusengine.core.CitrusEngine;\r\n\r\n\t[SWF(frameRate=\"60\")]\r\n\t\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class Main extends CitrusEngine {\r\n\t\t\r\n\t\tpublic function Main() {\r\n\r\n\t\t\t\/\/ import libraries from the libs folder, select just one Nape swc.\r\n\t\t\t\r\n\t\t\tstate = new OsmosGameState();\r\n\t\t}\r\n\t}\r\n}<\/pre>\n<p>The GameState :<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.osmos {\r\n\r\n\timport nape.geom.Vec2;\r\n\r\n\timport com.citrusengine.core.State;\r\n\timport com.citrusengine.objects.platformer.nape.Platform;\r\n\timport com.citrusengine.physics.Nape;\r\n\r\n\timport flash.display.DisplayObject;\r\n\timport flash.events.MouseEvent;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class OsmosGameState extends State {\r\n\t\t\r\n\t\tprivate var _clickedAtom:Atom;\r\n\r\n\t\tpublic function OsmosGameState() {\r\n\t\t\tsuper();\r\n\t\t}\r\n\r\n\t\toverride public function initialize():void {\r\n\r\n\t\t\tsuper.initialize();\r\n\r\n\t\t\tvar nape:Nape = new Nape(\"nape\", {gravity:new Vec2()});\r\n\t\t\t\/\/nape.visible = true;\r\n\t\t\tadd(nape);\r\n\t\t\t\r\n\t\t\tadd(new Platform(\"platformTop\", {x:0, y:-10, width:stage.stageWidth, height:10}));\r\n\t\t\tadd(new Platform(\"platformRight\", {x:stage.stageWidth, y:0, width:10, height:stage.stageHeight}));\r\n\t\t\tadd(new Platform(\"platformBot\", {x:0, y:stage.stageHeight, width:stage.stageWidth, height:10}));\r\n\t\t\tadd(new Platform(\"platformLeft\", {x:-10, y:0, width:10, height:stage.stageHeight}));\r\n\r\n\t\t\tvar atom:Atom, radius:Number;\r\n\t\t\t\r\n\t\t\tfor (var i:uint = 0; i < 10; ++i) {\r\n\r\n\t\t\t\tfor (var j:uint = 0; j < 10; ++j) {\r\n\t\t\t\t\t\r\n\t\t\t\t\tradius = 3 + Math.random() * 30;\r\n\t\t\t\t\tatom = new Atom(\"atom\"+i+j, {x:i * 50, y:j * 50, radius:radius, view:new AtomArt(radius), registration:\"topLeft\"});\r\n\t\t\t\t\tadd(atom);\r\n\t\t\t\t\t(view.getArt(atom) as DisplayObject).addEventListener(MouseEvent.MOUSE_DOWN, _handleGrab);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tstage.addEventListener(MouseEvent.MOUSE_UP, _handleRelease);\r\n\t\t}\r\n\r\n\t\tprivate function _handleGrab(mEvt:MouseEvent):void {\r\n\r\n\t\t\t_clickedAtom = view.getObjectFromArt(mEvt.currentTarget) as Atom;\r\n\r\n\t\t\tif (_clickedAtom)\r\n\t\t\t\t_clickedAtom.enableHolding(mEvt.currentTarget.parent);\r\n\t\t}\r\n\r\n\t\tprivate function _handleRelease(mEvt:MouseEvent):void {\r\n\t\t\t_clickedAtom.disableHolding();\r\n\t\t}\r\n\t}\r\n}<\/pre>\n<p>We removed gravity, create game limits and add many atoms. Each one can be drag\/dropped!<\/p>\n<p>The AtomArt class :<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.osmos {\r\n\r\n\timport flash.display.Sprite;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class AtomArt extends Sprite {\r\n\t\t\r\n\t\tprivate var _color:uint;\r\n\r\n\t\tpublic function AtomArt(radius:Number) {\r\n\t\t\t\r\n\t\t\t_color = Math.random() * 0xFFFFFF;\r\n\t\t\t\r\n\t\t\tthis.graphics.beginFill(_color);\r\n\t\t\tthis.graphics.drawCircle(0, 0, radius);\r\n\t\t\tthis.graphics.endFill();\r\n\t\t}\r\n\t\t\r\n\t\tpublic function changeSize(diameter:Number):void {\r\n\t\t\t\r\n\t\t\tthis.graphics.clear();\r\n\t\t\t\r\n\t\t\tif (diameter > 0) {\r\n\t\t\t\tthis.graphics.beginFill(_color);\r\n\t\t\t\tthis.graphics.drawCircle(0, 0, diameter * 0.5);\r\n\t\t\t\tthis.graphics.endFill();\r\n\t\t\t}\r\n\t\t} \r\n\t}\r\n}<\/pre>\n<p>The update will be called by the physics part.<br \/>\nThe Atom class :<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.osmos {\r\n\r\n\timport nape.callbacks.CbEvent;\r\n\timport nape.callbacks.CbType;\r\n\timport nape.callbacks.InteractionCallback;\r\n\timport nape.callbacks.InteractionListener;\r\n\timport nape.callbacks.InteractionType;\r\n\timport nape.callbacks.PreCallback;\r\n\timport nape.callbacks.PreFlag;\r\n\timport nape.callbacks.PreListener;\r\n\timport nape.constraint.PivotJoint;\r\n\timport nape.geom.Vec2;\r\n\timport nape.phys.Body;\r\n\timport nape.phys.BodyList;\r\n\r\n\timport com.citrusengine.objects.NapePhysicsObject;\r\n\r\n\timport flash.display.DisplayObject;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class Atom extends NapePhysicsObject {\r\n\t\t\r\n\t\tpublic static const ATOM:CbType = new CbType();\r\n\t\t\r\n\t\tpublic var size:String = \"\";\r\n\t\t\r\n\t\tprivate var _preListener:PreListener;\r\n\r\n\t\tprivate var _hand:PivotJoint;\r\n\t\tprivate var _mouseScope:DisplayObject;\r\n\r\n\t\tpublic function Atom(name:String, params:Object = null) {\r\n\r\n\t\t\tsuper(name, params);\r\n\t\t}\r\n\t\t\t\r\n\t\toverride protected function createConstraint():void {\r\n\t\t\t\r\n\t\t\tsuper.createConstraint();\r\n\t\t\t\r\n\t\t\t\/\/ we need to ignore physics collision\r\n\t\t\t_preListener = new PreListener(InteractionType.ANY, ATOM, ATOM, handlePreContact);\r\n\t\t\t_body.space.listeners.add(_preListener);\r\n\t\t\t_body.cbTypes.add(ATOM);\r\n\t\t\t\r\n\t\t\t_nape.space.listeners.add(new InteractionListener(CbEvent.ONGOING, InteractionType.ANY, ATOM, ATOM, handleOnGoingContact));\r\n\r\n\t\t\t_hand = new PivotJoint(_nape.space.world, null, new Vec2(), new Vec2());\r\n\t\t\t_hand.active = false;\r\n\t\t\t_hand.stiff = false;\r\n\t\t\t_hand.space = _nape.space;\r\n\t\t\t_hand.maxForce = 5;\r\n\t\t}\r\n\r\n\t\toverride public function destroy():void {\r\n\t\t\t\r\n\t\t\t_hand.space = null;\r\n\t\t\t\r\n\t\t\t_preListener.space = null;\r\n\t\t\t_preListener = null;\r\n\r\n\t\t\tsuper.destroy();\r\n\t\t}\r\n\r\n\t\toverride public function update(timeDelta:Number):void {\r\n\r\n\t\t\tsuper.update(timeDelta);\r\n\t\t\t\r\n\t\t\tif (_mouseScope)\r\n\t\t\t\t_hand.anchor1.setxy(_mouseScope.mouseX, _mouseScope.mouseY);\r\n\t\t\t\t\r\n\t\t\tvar bodyDiameter:Number;\r\n\t\t\t\t\r\n\t\t\tif (size == \"bigger\") {\r\n\t\t\t\tbodyDiameter = _body.shapes.at(0).bounds.width;\r\n\t\t\t\tbodyDiameter > 100 ? _body.scaleShapes(1.003, 1.003) : _body.scaleShapes(1.01, 1.01); \r\n\t\t\t\tbodyDiameter = _body.shapes.at(0).bounds.width;\r\n\t\t\t\t(view as AtomArt).changeSize(bodyDiameter);\r\n\t\t\t\t\r\n\t\t\t} else if (size == \"smaller\") {\r\n\t\t\t\t_body.scaleShapes(0.9, 0.9);\r\n\t\t\t\tbodyDiameter = _body.shapes.at(0).bounds.width;\r\n\t\t\t\t(view as AtomArt).changeSize(bodyDiameter);\r\n\t\t\t\t\r\n\t\t\t\tif (_body.shapes.at(0).bounds.width < 1)\r\n\t\t\t\t\tthis.kill = true;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tsize = \"\";\r\n\t\t}\r\n\r\n\t\tpublic function enableHolding(mouseScope:DisplayObject):void {\r\n\t\t\t\r\n\t\t\t_mouseScope = mouseScope;\r\n\t\t\t\r\n\t\t\tvar mp:Vec2 = new Vec2(mouseScope.mouseX, mouseScope.mouseY);\r\n            var bodies:BodyList = _nape.space.bodiesUnderPoint(mp);\r\n            for(var i:int = 0; i < bodies.length; ++i) {\r\n                var b:Body = bodies.at(i);\r\n                if(!b.isDynamic()) continue;\r\n                _hand.body2 = b;\r\n                _hand.anchor2 = b.worldToLocal(mp);\r\n                _hand.active = true;\r\n                break;\r\n            }\r\n\t\t\t\r\n\t\t}\r\n\r\n\t\tpublic function disableHolding():void {\r\n\t\t\t\r\n\t\t\t_hand.active = false;\r\n\t\t\t_mouseScope = null;\r\n\t\t}\r\n\t\t\t\r\n\t\tprotected function handleOnGoingContact(callback:InteractionCallback):void {\r\n\t\t\t\r\n\t\t\tvar atom1:Atom = callback.int1.userData.myData as Atom;\r\n\t\t\tvar atom2:Atom = callback.int2.userData.myData as Atom;\r\n\t\t\t\r\n\t\t\tif (atom1.body.shapes.at(0).bounds.width > atom2.body.shapes.at(0).bounds.width) {\r\n\t\t\t\t atom1.size = \"bigger\";\r\n\t\t\t\t atom2.size = \"smaller\";\r\n\t\t\t} else {\r\n\t\t\t\tatom1.size = \"smaller\";\r\n\t\t\t\tatom2.size = \"bigger\";\r\n\t\t\t}\r\n\t\t}\r\n\t\t\t\r\n\t\toverride public function handlePreContact(callback:PreCallback):PreFlag {\r\n\t\t\t\r\n\t\t\treturn PreFlag.IGNORE;\r\n\t\t}\r\n\r\n\t}\r\n}<\/pre>\n<p>If the atom is too small, it is destroyed. Again take a look on <a href=\"http:\/\/deltaluca.me.uk\/docnew\/\" target=\"_blank\">Nape documentation<\/a> if you have some trouble. It wasn't the CE part, the difficult one, isn't it?<\/p>\n<p>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!<\/p>\n<p>That was a non platformer game tutorial for the CE \ud83d\ude00 I hope to be able to provide more soon! Which game would you like?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>2 days ago, I&#8217;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&#8217;s time to show, thanks to a quick case study, that the Citrus Engine is not &hellip; <a href=\"http:\/\/www.aymericlamboley.fr\/blog\/create-a-game-like-osmos\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">Create a game like Osmos<\/span> <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0},"categories":[4,51,33,11,145,114,6],"tags":[15,50,34,26,146,74],"_links":{"self":[{"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/posts\/714"}],"collection":[{"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/comments?post=714"}],"version-history":[{"count":7,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/posts\/714\/revisions"}],"predecessor-version":[{"id":1273,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/posts\/714\/revisions\/1273"}],"wp:attachment":[{"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/media?parent=714"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/categories?post=714"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/tags?post=714"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}