{"id":721,"date":"2012-08-31T16:36:42","date_gmt":"2012-08-31T15:36:42","guid":{"rendered":"http:\/\/www.aymericlamboley.fr\/blog\/?p=721"},"modified":"2014-11-01T14:51:07","modified_gmt":"2014-11-01T13:51:07","slug":"live4sales-a-plant-vs-zombies-clone","status":"publish","type":"post","link":"http:\/\/www.aymericlamboley.fr\/blog\/live4sales-a-plant-vs-zombies-clone\/","title":{"rendered":"Live4Sales, a Plant vs Zombies clone"},"content":{"rendered":"<p><em>Edit : <a href=\"http:\/\/www.aymericlamboley.fr\/blog\/box2d-alchemy-vs-nape-performance-test-on-ipad3\/\" target=\"_blank\">Nape version<\/a><\/em><\/p>\n<p>One week after the <a href=\"http:\/\/www.aymericlamboley.fr\/blog\/create-a-game-like-osmos\/\" target=\"_blank\">Osmos demo<\/a>, I offer a new game made with the<a href=\"http:\/\/citrusengine.com\/\" target=\"_blank\"> Citrus Engine<\/a> based on an other popular game : Plant vs. Zombies!<\/p>\n<p>If you have never played this game, <a href=\"http:\/\/www.popcap.com\/games\/plants-vs-zombies\/online\" target=\"_blank\">try it now!<\/a><\/p>\n<p>This is my clone made with the CE working as a browser game, air application, and on mobile devices :<br \/>\n<a href=\"http:\/\/www.aymericlamboley.fr\/blog\/wp-content\/uploads\/2012\/08\/Live4Sales.html\" target=\"_blank\">The web demo.<\/a> I didn\u2019t make a preloader, to have the same source code between each version.<br \/>\n<strong>There is no end in the game to be used as a stress test!<\/strong><br \/>\n<em>Sources are available on the CE&#8217;s <a href=\"https:\/\/github.com\/alamboley\/Citrus-Engine\" target=\"_blank\">GitHub<\/a>.<\/em><\/p>\n<p><iframe loading=\"lazy\" src=\"http:\/\/player.vimeo.com\/video\/48609477\" width=\"500\" height=\"281\" frameborder=\"0\" webkitAllowFullScreen mozallowfullscreen allowFullScreen><\/iframe><br \/>\n<iframe loading=\"lazy\" src=\"http:\/\/player.vimeo.com\/video\/48609476\" width=\"500\" height=\"281\" frameborder=\"0\" webkitAllowFullScreen mozallowfullscreen allowFullScreen><\/iframe><\/p>\n<p><!--more--><\/p>\n<p><strong>The story part<\/strong><br \/>\nI was in Paris last weekend with <a href=\"https:\/\/twitter.com\/pierrickpluchon\" target=\"_blank\">Pierrick<\/a>. We planned to make the <a href=\"http:\/\/www.ludumdare.com\/\" target=\"_blank\">Ludum Dare<\/a> contest. The theme was evolution. We aren&#8217;t graphics designer, it&#8217;s (always) a problem. So we used graphics from a previous Pierrick&#8217;s project, the graphic designer was Lou Drouet. We thought to evolve ours shoppers into zombies to criticize brands addiction. Salers would throw later smartphones, etc&#8230; Nothing original, but funny. We didn&#8217;t finish the game because on Saturday&#8217;s night, my macbook pro died. It was baptised by beer and didn&#8217;t survive. After 4-5 years of excellent services, it was the best end that could happen to it : death on the battlefield! R.I.P. Since I need a laptop, I&#8217;ve bought a cheap Acer which do the job, I planned to buy an iMac later. No more Objective-C &#038; Haxe NME for mobile at the moment. That&#8217;s it for my life part.<\/p>\n<p><strong>The case study<\/strong><br \/>\nPlant vs Zombies is an interesting programming lesson since there are different ways to approach it. Some years ago<br \/>\n<a href=\"http:\/\/www.emanueleferonato.com\/2010\/12\/29\/making-a-flash-game-like-plants-vs-zombies\/\" target=\"_blank\">Emanuele Feronato has made several tutorials<\/a> to develop this game. I really enjoy informations on his blog but don&#8217;t like his way of programming, not enough OOP. Anyway, I invite you to read his blog.<\/p>\n<p>To develop Live4Sales, I used as usual the Citrus Engine. To detect collision I used Box2D physics engine ; Nape would work better but I wanted to test Box2D&#8217;s performances on mobile! Also you would probably not use a physics engine at all to make only simple collision detection. And finally I used <a href=\"http:\/\/gamua.com\/starling\/\" target=\"_blank\">Starling<\/a> for the view renderer.<\/p>\n<p>The physics engine is very useful for collision detection, and don&#8217;t forget : graphics always lie! The physic body is smaller than the graphics so we don&#8217;t have to manage\/ignore layers superposition. Even if we use a physics engine, we need a grid to prevent two objects on the same case and to know if baddies are on a line. So basically we use 2 array.<\/p>\n<p>Since there are lots of codes and class, I wouldn&#8217;t explain them all. We will focus on the game loop, a part of the hud (the drag &#038; drop), the IA class and finally Box2D&#8217;s objects classes with some simple optimizations you have to take care!<\/p>\n<p>Our GameState :<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.live4sales {\r\n\r\n\timport Box2DAS.Common.V2;\r\n\r\n\timport games.live4sales.assets.Assets;\r\n\timport games.live4sales.characters.SalesWoman;\r\n\timport games.live4sales.events.MoneyEvent;\r\n\timport games.live4sales.objects.Block;\r\n\timport games.live4sales.objects.Cash;\r\n\timport games.live4sales.runtime.BaddiesCreation;\r\n\timport games.live4sales.runtime.CoinsCreation;\r\n\timport games.live4sales.ui.Hud;\r\n\timport games.live4sales.utils.Grid;\r\n\r\n\timport starling.core.Starling;\r\n\timport starling.display.Image;\r\n\r\n\timport com.citrusengine.core.CitrusEngine;\r\n\timport com.citrusengine.core.StarlingState;\r\n\timport com.citrusengine.objects.CitrusSprite;\r\n\timport com.citrusengine.physics.Box2D;\r\n\timport com.citrusengine.view.starlingview.AnimationSequence;\r\n\timport com.citrusengine.view.starlingview.StarlingArt;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class Live4Sales extends StarlingState {\r\n\r\n\t\tprivate var _ce:CitrusEngine;\r\n\t\tprivate var _hud:Hud;\r\n\t\tprivate var _coinsCreation:CoinsCreation;\r\n\t\tprivate var _baddiesCreation:BaddiesCreation;\r\n\r\n\t\tpublic function Live4Sales() {\r\n\t\t\t\r\n\t\t\tsuper();\r\n\r\n\t\t\t_ce = CitrusEngine.getInstance();\r\n\t\t}\r\n\r\n\t\toverride public function initialize():void {\r\n\t\t\t\r\n\t\t\tsuper.initialize();\r\n\r\n\t\t\tAssets.contentScaleFactor = Starling.current.contentScaleFactor;\r\n\r\n\t\t\tvar box2D:Box2D = new Box2D(\"box2D\", {gravity:new V2()});\r\n\t\t\t\/\/box2D.visible = true;\r\n\t\t\tadd(box2D);\r\n\r\n\t\t\t_hud = new Hud();\r\n\t\t\taddChild(_hud);\r\n\t\t\t_hud.onIconePositioned.add(_createObject);\r\n\r\n\t\t\t_coinsCreation = new CoinsCreation();\r\n\t\t\taddChild(_coinsCreation);\r\n\r\n\t\t\tStarlingArt.setLoopAnimations([\"stand\", \"fire\", \"attack\", \"walk\"]);\r\n\r\n\t\t\tvar background:CitrusSprite = new CitrusSprite(\"background\", {view:Image.fromBitmap(new Assets.BackgroundPng())});\r\n\t\t\tadd(background);\r\n\r\n\t\t\t_baddiesCreation = new BaddiesCreation();\r\n\t\t}\r\n\r\n\t\tprivate function _createObject(name:String, posX:uint, posY:uint):void {\r\n\t\t\t\r\n\t\t\tif (Hud.money >= 50) {\r\n\t\t\t\t\r\n\t\t\t\tvar casePositions:Array = Grid.getCaseCenter(posX, posY);\r\n\r\n\t\t\t\tif (casePositions[0] != 0 && casePositions[1] != 0) {\r\n\t\t\t\t\t\r\n\t\t\t\t\tif (name == \"SalesWoman\") {\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tvar saleswomanAnim:AnimationSequence = new AnimationSequence(Assets.getTextureAtlas(\"Objects\"), [\"fire\", \"stand\"], \"fire\", 30, true);\r\n\t\t\t\t\t\tvar saleswoman:SalesWoman = new SalesWoman(\"saleswoman\", {x:casePositions[0], y:casePositions[1], group:casePositions[2], offsetY:-saleswomanAnim.height * 0.3, fireRate:1000, missileExplodeDuration:0, missileFuseDuration:3000, view:saleswomanAnim});\r\n\t\t\t\t\t\tadd(saleswoman);\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t} else if (name == \"Block\") {\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tvar blockAnimation:AnimationSequence = new AnimationSequence(Assets.getTextureAtlas(\"Objects\"), [\"block1\", \"block2\", \"block3\", \"blockDestroyed\"], \"block1\");\r\n\t\t\t\t\t\tvar block:Block = new Block(\"block\", {x:casePositions[0], y:casePositions[1], group:casePositions[2], offsetY:-15, view:blockAnimation});\r\n\t\t\t\t\t\tadd(block);\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t} else if (name == \"Cash\") {\r\n\t\t\t\t\t\t\r\n\t\t\t\t\t\tvar cash:Cash = new Cash(\"cash\", {x:casePositions[0], y:casePositions[1], group:casePositions[2], offsetY:-15, view:new Image(Assets.getAtlasTexture(\"cash\", \"Objects\"))});\r\n\t\t\t\t\t\tadd(cash);\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\t_ce.dispatchEvent(new MoneyEvent(MoneyEvent.BUY_ITEM));\r\n\t\t\t\t\t\r\n\t\t\t\t} else trace('case not empty');\r\n\t\t\t\t\r\n\t\t\t} else trace('no money');\r\n\t\t}\r\n\t}\r\n}<\/pre>\n<p>We create the Hud, the CoinsCreation &#038; BaddiesCreation classes to create coins\/baddies at runtime and we will create there our objects if we have money and if the case is empty.<\/p>\n<p><strong>Two importants CE&#8217;s properties<\/strong><br \/>\n&#8211; the group property is used as a z-index sorter.<br \/>\n&#8211; thanks to the offsetX &#038; offsetY properties even if your graphics is registred at the center point, you can move around!<\/p>\n<p>The Hud :<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.live4sales.ui {\r\n\r\n\timport games.live4sales.events.MoneyEvent;\r\n\timport games.live4sales.assets.Assets;\r\n\timport games.live4sales.utils.Grid;\r\n\r\n\timport starling.display.Image;\r\n\timport starling.display.Sprite;\r\n\timport starling.events.Event;\r\n\timport starling.text.BitmapFont;\r\n\timport starling.text.TextField;\r\n\timport starling.textures.Texture;\r\n\r\n\timport com.citrusengine.core.CitrusEngine;\r\n\r\n\timport org.osflash.signals.Signal;\r\n\r\n\timport flash.display.Bitmap;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class Hud extends Sprite {\r\n\t\t\r\n\t\t[Embed(source=\"..\/embed\/ArialFont.fnt\", mimeType=\"application\/octet-stream\")] private var _fontConfig:Class;\r\n\t\t[Embed(source=\"..\/embed\/ArialFont.png\")] private var _fontPng:Class;\r\n\t\t\r\n\t\tstatic public var money:uint = 400;\r\n\r\n\t\tpublic var onIconePositioned:Signal;\r\n\t\t\r\n\t\tprivate var _ce:CitrusEngine;\r\n\r\n\t\tprivate var _grid:Grid;\r\n\t\tprivate var _vectorIcon:Vector.<Icon>;\r\n\r\n\t\tprivate var _backgroundMenu:Image;\r\n\t\tprivate var _iconSaleswoman:Icon;\r\n\t\tprivate var _iconCash:Icon;\r\n\t\tprivate var _iconBlock:Icon;\r\n\t\t\r\n\t\tprivate var _score:TextField;\r\n\r\n\t\tpublic function Hud() {\r\n\t\t\t\r\n\t\t\t_ce = CitrusEngine.getInstance();\r\n\r\n\t\t\tonIconePositioned = new Signal(String, uint, uint);\r\n\r\n\t\t\t_grid = new Grid();\r\n\r\n\t\t\t_vectorIcon = new Vector.<Icon>(3, true);\r\n\r\n\t\t\t_backgroundMenu = new Image(Assets.getAtlasTexture(\"background-menu\", \"Menu\"));\r\n\t\t\t_iconSaleswoman = new Icon(Assets.getAtlasTexture(\"icon-saleswoman\", \"Menu\"));\r\n\t\t\t_iconCash = new Icon(Assets.getAtlasTexture(\"icon-cash\", \"Menu\"));\r\n\t\t\t_iconBlock = new Icon(Assets.getAtlasTexture(\"icon-block\", \"Menu\"));\r\n\r\n\t\t\t_vectorIcon[0] = _iconSaleswoman;\r\n\t\t\t_vectorIcon[1] = _iconCash;\r\n\t\t\t_vectorIcon[2] = _iconBlock;\r\n\t\t\t\r\n\t\t\tvar bitmap:Bitmap = new _fontPng();\r\n\t\t\tvar texture:Texture = Texture.fromBitmap(bitmap);\r\n\t\t\tvar xml:XML = XML(new _fontConfig());\r\n\t\t\tTextField.registerBitmapFont(new BitmapFont(texture, xml));\r\n\t\t\t_score = new TextField(50, 20, \"0\", \"ArialMT\");\r\n\r\n\t\t\taddEventListener(Event.ADDED_TO_STAGE, _addedToStage);\r\n\t\t}\r\n\r\n\t\tpublic function destroy():void {\r\n\t\t\t\r\n\t\t\tonIconePositioned.removeAll();\r\n\t\t\t\r\n\t\t\tremoveChild(_grid);\r\n\t\t\t\r\n\t\t\tfor each (var icon:Icon in _vectorIcon) {\r\n\t\t\t\ticon.destroy();\r\n\t\t\t\tremoveChild(icon);\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t_vectorIcon = null;\r\n\t\t\t\r\n\t\t\tTextField.unregisterBitmapFont(\"ArialMT\");\r\n\t\t\tremoveChild(_score);\r\n\t\t\t\r\n\t\t\t_ce.removeEventListener(MoneyEvent.BUY_ITEM, _changeMoney);\r\n\t\t}\r\n\r\n\t\tprivate function _addedToStage(evt:Event):void {\r\n\r\n\t\t\tremoveEventListener(Event.ADDED_TO_STAGE, _addedToStage);\r\n\r\n\t\t\taddChild(_grid);\r\n\t\t\t_grid.visible = false;\r\n\r\n\t\t\taddChild(_backgroundMenu);\r\n\t\t\t_backgroundMenu.x = (480 - _backgroundMenu.width) \/ 2;\r\n\r\n\t\t\taddChild(_iconSaleswoman);\r\n\t\t\t_iconSaleswoman.name = \"SalesWoman\";\r\n\t\t\t_iconSaleswoman.x = (480 - _iconSaleswoman.width) \/ 2 - 30;\r\n\t\t\t\r\n\t\t\taddChild(_iconCash);\r\n\t\t\t_iconCash.name = \"Cash\";\r\n\t\t\t_iconCash.x = (480 - _iconCash.width) \/ 2 + 20;\r\n\t\t\t\r\n\t\t\taddChild(_iconBlock);\r\n\t\t\t_iconBlock.name = \"Block\";\r\n\t\t\t_iconBlock.x = (480 - _iconBlock.width) \/ 2 + 70;\r\n\r\n\t\t\tfor each (var icon:Icon in _vectorIcon) {\r\n\t\t\t\ticon.onStartDrag.add(_showGrid);\r\n\t\t\t\ticon.onStopDrag.add(_hideGridAndCreateObject);\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\t_score.x = 150;\r\n\t\t\t_score.y = 3;\r\n\t\t\taddChild(_score);\r\n\t\t\t_score.text = String(money);\r\n\t\t\t\r\n\t\t\t_ce.addEventListener(MoneyEvent.BUY_ITEM, _changeMoney);\r\n\t\t\t_ce.addEventListener(MoneyEvent.PICKUP_MONEY, _changeMoney);\r\n\t\t}\r\n\r\n\t\tprivate function _changeMoney(mEvt:MoneyEvent):void {\r\n\t\t\t\r\n\t\t\tif (mEvt.type == \"BUY_ITEM\")\r\n\t\t\t\tmoney -= 50;\r\n\t\t\telse if (mEvt.type == \"PICKUP_MONEY\")\r\n\t\t\t\tmoney += 50;\r\n\t\t\t\r\n\t\t\t_score.text = String(money);\r\n\t\t}\r\n\r\n\t\tprivate function _showGrid():void {\r\n\t\t\t_grid.visible = true;\r\n\t\t}\r\n\r\n\t\tprivate function _hideGridAndCreateObject(name:String, posX:uint, posY:uint):void {\r\n\t\t\t_grid.visible = false;\r\n\t\t\tonIconePositioned.dispatch(name, posX, posY);\r\n\t\t}\r\n\t}\r\n}<\/pre>\n<p>The Icon :<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.live4sales.ui {\r\n\r\n\timport starling.display.Image;\r\n\timport starling.events.Touch;\r\n\timport starling.events.TouchEvent;\r\n\timport starling.events.TouchPhase;\r\n\timport starling.textures.Texture;\r\n\r\n\timport org.osflash.signals.Signal;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class Icon extends Image {\r\n\t\t\r\n\t\tpublic var onStartDrag:Signal;\r\n\t\tpublic var onStopDrag:Signal;\r\n\r\n\t\tprivate var _dragging:Boolean = false;\r\n\r\n\t\tprivate var _posX:uint, _posY:uint;\r\n\r\n\t\tpublic function Icon(texture:Texture) {\r\n\t\t\t\r\n\t\t\tsuper(texture);\r\n\t\t\t\r\n\t\t\tonStartDrag = new Signal();\r\n\t\t\tonStopDrag = new Signal(String, uint, uint);\r\n\t\t\t\r\n\t\t\taddEventListener(TouchEvent.TOUCH, _iconTouched);\r\n\t\t}\r\n\t\t\r\n\t\tpublic function destroy():void {\r\n\t\t\t\r\n\t\t\tremoveEventListener(TouchEvent.TOUCH, _iconTouched);\r\n\t\t\t\r\n\t\t\tonStartDrag.removeAll();\r\n\t\t\tonStopDrag.removeAll();\r\n\t\t}\r\n\r\n\t\tprivate function _iconTouched(tEvt:TouchEvent):void {\r\n\r\n\t\t\tvar touchBegan:Touch = tEvt.getTouch(this, TouchPhase.BEGAN);\r\n\t\t\tvar touchMoved:Touch = tEvt.getTouch(this, TouchPhase.MOVED);\r\n\t\t\tvar touchEnded:Touch = tEvt.getTouch(this, TouchPhase.ENDED);\r\n\r\n\t\t\tif (touchBegan) {\r\n\t\t\t\t\r\n\t\t\t\t_posX = this.x;\r\n\t\t\t\t_posY = this.y;\r\n\t\t\t\t\r\n\t\t\t\t_dragging = true;\r\n\t\t\t\t\r\n\t\t\t\tonStartDrag.dispatch();\r\n\r\n\t\t\t} else if (touchMoved && _dragging) {\r\n\r\n\t\t\t\tthis.x = touchMoved.globalX - this.width * 0.5;\r\n\t\t\t\tthis.y = touchMoved.globalY - this.height * 0.5;\r\n\r\n\t\t\t} else if (touchEnded && _dragging) {\r\n\t\t\t\t\r\n\t\t\t\t_dragging = false;\r\n\t\t\t\t\r\n\t\t\t\tthis.x = _posX;\r\n\t\t\t\tthis.y = _posY;\r\n\t\t\t\t\r\n\t\t\t\tonStopDrag.dispatch(name, uint(touchEnded.globalX), uint(touchEnded.globalY));\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n}<\/pre>\n<p>In a game like Plant vs Zombies, the difficulty is progressive, there are more and more enemies. We use a master timer which will change the enemy&#8217;s speed creation after each 10s. It will be reduced of 500ms.<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.live4sales.runtime {\r\n\r\n\timport games.live4sales.assets.Assets;\r\n\timport games.live4sales.characters.ShopsWoman;\r\n\timport games.live4sales.utils.Grid;\r\n\r\n\timport com.citrusengine.core.CitrusEngine;\r\n\timport com.citrusengine.view.starlingview.AnimationSequence;\r\n\r\n\timport flash.events.TimerEvent;\r\n\timport flash.utils.Timer;\r\n\t\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class BaddiesCreation {\r\n\t\t\r\n\t\tprivate var _ce:CitrusEngine;\r\n\t\t\r\n\t\tprivate var _timerProgression:Timer;\r\n\t\t\r\n\t\tprivate var _timerShopsWomen:Timer;\r\n\t\t\r\n\t\tpublic function BaddiesCreation() {\r\n\t\t\t\r\n\t\t\t_ce = CitrusEngine.getInstance();\r\n\t\t\t\r\n\t\t\t_timerProgression = new Timer(10000);\r\n\t\t\t_timerProgression.start();\r\n\t\t\t_timerProgression.addEventListener(TimerEvent.TIMER, _progressionDifficulty);\r\n\t\t\t\r\n\t\t\t_timerShopsWomen = new Timer(4000);\r\n\t\t\t_timerShopsWomen.start();\r\n\t\t\t_timerShopsWomen.addEventListener(TimerEvent.TIMER, _tick);\r\n\t\t}\r\n\t\t\r\n\t\tpublic function destroy():void {\r\n\t\t\t\r\n\t\t\t_timerProgression.stop();\r\n\t\t\t_timerProgression.removeEventListener(TimerEvent.TIMER, _progressionDifficulty);\r\n\t\t\t\r\n\t\t\t_timerShopsWomen.stop();\r\n\t\t\t_timerShopsWomen.removeEventListener(TimerEvent.TIMER, _tick);\r\n\t\t}\r\n\t\t\r\n\t\tprivate function _progressionDifficulty(tEvt:TimerEvent):void {\r\n\t\t\t\r\n\t\t\t_timerShopsWomen.removeEventListener(TimerEvent.TIMER, _tick);\r\n\t\t\t\r\n\t\t\tvar delay:uint = _timerShopsWomen.delay - 500;\r\n\t\t\tif (delay < 500)\r\n\t\t\t\tdelay = 500;\r\n\t\t\t\r\n\t\t\t_timerShopsWomen = new Timer(delay);\r\n\t\t\t_timerShopsWomen.start();\r\n\t\t\t_timerShopsWomen.addEventListener(TimerEvent.TIMER, _tick);\r\n\t\t}\r\n\r\n\t\tprivate function _tick(tEvt:TimerEvent):void {\r\n\t\t\t\r\n\t\t\tvar casePosition:Array = Grid.getBaddyPosition(0, Grid.getRandomHeight());\r\n\t\t\t\r\n\t\t\tvar shopsWomanAnim:AnimationSequence = new AnimationSequence(Assets.getTextureAtlas(\"Objects\"), [\"walk\", \"attack\"], \"walk\");\r\n\t\t\tvar shopswoman:ShopsWoman = new ShopsWoman(\"shopswoman\", {x:480, y:casePosition[1], group:casePosition[2],  offsetY:-shopsWomanAnim.height * 0.3, view:shopsWomanAnim});\r\n\t\t\t_ce.state.add(shopswoman);\r\n\t\t\tshopswoman.onTouchLeftSide.add(_endGame);\r\n\t\t\t\r\n\t\t\tGrid.tabBaddies[casePosition[2]] = true;\r\n\t\t}\r\n\t\t\r\n\t\tprivate function _endGame():void {\r\n\r\n\t\t\ttrace('game over');\r\n\t\t}\r\n\t}\r\n}<\/pre>\n<p>Now let's start some Box2D stuff. The Citrus Engine has already lots of Box2D objects pre-built, we will extend them.<br \/>\nExample the SalesWoman which is just similar to a Cannon :<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.live4sales.characters {\r\n\r\n\timport Box2DAS.Dynamics.ContactEvent;\r\n\r\n\timport games.live4sales.assets.Assets;\r\n\timport games.live4sales.utils.Grid;\r\n\timport games.live4sales.weapons.Bag;\r\n\r\n\timport starling.display.Image;\r\n\r\n\timport com.citrusengine.objects.platformer.box2d.Cannon;\r\n\r\n\timport flash.events.TimerEvent;\r\n\timport flash.utils.Timer;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class SalesWoman extends Cannon {\r\n\t\t\r\n\t\tpublic var life:uint = 2;\r\n\t\t\r\n\t\tprivate var _timerHurt:Timer;\r\n\r\n\t\tpublic function SalesWoman(name:String, params:Object = null) {\r\n\t\t\t\r\n\t\t\tsuper(name, params);\r\n\t\t\t\r\n\t\t\t_timerHurt = new Timer(1000);\r\n\t\t\t_timerHurt.addEventListener(TimerEvent.TIMER, _removeLife);\r\n\t\t}\r\n\t\t\t\r\n\t\toverride public function destroy():void {\r\n\t\t\t\r\n\t\t\t_fixture.removeEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact);\r\n\t\t\t_fixture.removeEventListener(ContactEvent.END_CONTACT, handleEndContact);\r\n\t\t\t\r\n\t\t\t_timerHurt.removeEventListener(TimerEvent.TIMER, _removeLife);\r\n\t\t\t_timerHurt = null;\r\n\t\t\t\r\n\t\t\tsuper.destroy();\r\n\t\t}\r\n\r\n\t\toverride public function update(timeDelta:Number):void {\r\n\t\t\t\r\n\t\t\tsuper.update(timeDelta);\r\n\t\t\t\r\n\t\t\tif (life == 0) {\r\n\t\t\t\tkill = true;\r\n\t\t\t\tvar tab:Array = Grid.getCaseId(x, y);\r\n\t\t\t\t\r\n\t\t\t\tGrid.tabObjects[tab[1]][tab[0]] = false;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tif (Grid.tabBaddies[group])\r\n\t\t\t\t_firing = true;\r\n\t\t\telse\r\n\t\t\t\t_firing = false;\r\n\t\t}\r\n\t\t\t\r\n\t\toverride protected function createFixture():void {\r\n\t\t\t\r\n\t\t\t_fixture = _body.CreateFixture(_fixtureDef);\r\n\t\t\t_fixture.m_reportBeginContact = true;\r\n\t\t\t_fixture.m_reportEndContact = true;\r\n\t\t\t_fixture.addEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact);\r\n\t\t\t_fixture.addEventListener(ContactEvent.END_CONTACT, handleEndContact);\r\n\t\t}\r\n\t\t\r\n\t\tprotected function handleBeginContact(cEvt:ContactEvent):void {\r\n\t\t\t\r\n\t\t\tif (cEvt.other.GetBody().GetUserData() is ShopsWoman) {\r\n\t\t\t\t\r\n\t\t\t\tif (!_timerHurt.running)\r\n\t\t\t\t\t_timerHurt.start();\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\tprotected function handleEndContact(cEvt:ContactEvent):void {\r\n\t\t\t\r\n\t\t\tif (cEvt.other.GetBody().GetUserData() is ShopsWoman) {\r\n\t\t\t\t\r\n\t\t\t\tif (_timerHurt.running)\r\n\t\t\t\t\t_timerHurt.stop();\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\tprivate function _removeLife(tEvt:TimerEvent):void {\r\n\t\t\tlife--;\r\n\t\t}\r\n\t\t\r\n\t\toverride protected function _fire(tEvt:TimerEvent):void {\r\n\t\t\t\r\n\t\t\tif (_firing) {\r\n\r\n\t\t\t\tvar missile:Bag;\r\n\t\r\n\t\t\t\tif (startingDirection == \"right\")\r\n\t\t\t\t\tmissile = new Bag(\"Missile\", {x:x + width, y:y, group:group, width:missileWidth, height:missileHeight, offsetY:-30, speed:missileSpeed, angle:missileAngle, explodeDuration:missileExplodeDuration, fuseDuration:missileFuseDuration, view:new Image(Assets.getAtlasTexture(\"bag\", \"Objects\"))});\r\n\t\t\t\telse\r\n\t\t\t\t\tmissile = new Bag(\"Missile\", {x:x - width, y:y, group:group, width:missileWidth, height:missileHeight, offsetY:-30, speed:-missileSpeed, angle:missileAngle, explodeDuration:missileExplodeDuration, fuseDuration:missileFuseDuration, view:new Image(Assets.getAtlasTexture(\"bag\", \"Objects\"))});\r\n\t\r\n\t\t\t\t_ce.state.add(missile);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\toverride protected function _updateAnimation():void {\r\n\t\t\t\r\n\t\t\tif (_firing)\r\n\t\t\t\t_animation = \"fire\";\r\n\t\t\telse\r\n\t\t\t\t_animation = \"stand\";\r\n\t\t}\r\n\r\n\t}\r\n}<\/pre>\n<p>Now let's start talking about some Box2D optimizations :<br \/>\nA missile should only collide with an enemy, we can do it easily :<\/p>\n<pre lang=\"actionscript3\">protected function handleBeginContact(cEvt:ContactEvent):void {\r\n\t\t\t\r\n\tvar other:Box2DPhysicsObject = cEvt.other.GetBody().GetUserData();\r\n\r\n\tif (!(other is ShopsWoman))\r\n\t\tcEvt.contact.Disable();\r\n}<\/pre>\n<p>However disabling a contact this way is expensive if we have lots of missiles & contacts. Imagine, a missile crosses through an other SalesWoman, Cash and Block... So we use categoryBits & maskBits filters! Then they will never collide! With the Citrus Engine we can set up filters easily without bits manipulation. Also ShopsWoman shoudln't collide between them.<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.live4sales.characters {\r\n\r\n\timport Box2DAS.Common.V2;\r\n\timport Box2DAS.Dynamics.ContactEvent;\r\n\r\n\timport games.live4sales.objects.Block;\r\n\timport games.live4sales.objects.Cash;\r\n\timport games.live4sales.utils.Grid;\r\n\timport games.live4sales.weapons.Bag;\r\n\r\n\timport com.citrusengine.objects.Box2DPhysicsObject;\r\n\timport com.citrusengine.physics.Box2DCollisionCategories;\r\n\r\n\timport org.osflash.signals.Signal;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class ShopsWoman extends Box2DPhysicsObject {\r\n\t\t\r\n\t\tpublic var speed:Number = 0.7;\r\n\t\tpublic var life:uint = 4;\r\n\t\t\r\n\t\tpublic var onTouchLeftSide:Signal;\r\n\t\t\r\n\t\tprivate var _fighting:Boolean = false;\r\n\r\n\t\tpublic function ShopsWoman(name:String, params:Object = null) {\r\n\t\t\t\r\n\t\t\tsuper(name, params);\r\n\t\t\t\r\n\t\t\tonTouchLeftSide = new Signal();\r\n\t\t}\r\n\t\t\t\r\n\t\toverride public function destroy():void {\r\n\t\t\t\r\n\t\t\t_fixture.removeEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact);\r\n\t\t\t_fixture.removeEventListener(ContactEvent.END_CONTACT, handleEndContact);\r\n\t\t\t\r\n\t\t\tonTouchLeftSide.removeAll();\r\n\t\t\t\r\n\t\t\tsuper.destroy();\r\n\t\t}\r\n\t\t\t\r\n\t\toverride public function update(timeDelta:Number):void {\r\n\t\t\t\r\n\t\t\tsuper.update(timeDelta);\r\n\t\t\t\r\n\t\t\tif (!_fighting) {\r\n\t\t\t\r\n\t\t\t\tvar velocity:V2 = _body.GetLinearVelocity();\r\n\t\t\t\t\r\n\t\t\t\tvelocity.x = -speed;\r\n\t\t\t\t\r\n\t\t\t\t_body.SetLinearVelocity(velocity);\r\n\t\t\t}\r\n\t\t\t\t\r\n\t\t\tif (x < 0) {\r\n\t\t\t\tonTouchLeftSide.dispatch();\r\n\t\t\t\tkill = true;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tif (life == 0) {\r\n\t\t\t\tkill = true;\r\n\t\t\t\tGrid.tabBaddies[group] = false;\r\n\t\t\t} else {\r\n\t\t\t\tGrid.tabBaddies[group] = true;\r\n\t\t\t}\r\n\t\t\t\r\n\t\t\tupdateAnimation();\r\n\t\t}\r\n\t\t\r\n\t\toverride protected function defineBody():void {\r\n\t\t\t\r\n\t\t\tsuper.defineBody();\r\n\t\t\t\r\n\t\t\t_bodyDef.fixedRotation = true;\r\n\t\t}\r\n\t\t\r\n\t\toverride protected function defineFixture():void {\r\n\t\t\t\r\n\t\t\tsuper.defineFixture();\r\n\t\t\t\r\n\t\t\t_fixtureDef.friction = 0;\r\n\t\t\t_fixtureDef.filter.categoryBits = Box2DCollisionCategories.Get(\"BadGuys\");\r\n\t\t\t_fixtureDef.filter.maskBits = Box2DCollisionCategories.GetAllExcept(\"BadGuys\");\r\n\t\t}\r\n\t\t\t\r\n\t\toverride protected function createFixture():void {\r\n\t\t\t\r\n\t\t\tsuper.createFixture();\r\n\t\t\t\r\n\t\t\t_fixture.m_reportBeginContact = true;\r\n\t\t\t_fixture.m_reportEndContact = true;\r\n\t\t\t_fixture.addEventListener(ContactEvent.BEGIN_CONTACT, handleBeginContact);\r\n\t\t\t_fixture.addEventListener(ContactEvent.END_CONTACT, handleEndContact);\r\n\t\t}\r\n\t\t\t\r\n\t\tprotected function handleBeginContact(cEvt:ContactEvent):void {\r\n\t\t\t\r\n\t\t\tvar other:Box2DPhysicsObject = cEvt.other.GetBody().GetUserData();\r\n\t\t\t\r\n\t\t\tif (other is SalesWoman || other is Block || other is Cash)\r\n\t\t\t\t_fighting = true;\r\n\t\t\t\t\r\n\t\t\telse if (other is Bag) {\r\n\t\t\t\tlife--;\r\n\t\t\t\tcEvt.contact.Disable();\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\tprotected function handleEndContact(cEvt:ContactEvent):void {\r\n\t\t\t\r\n\t\t\tvar other:Box2DPhysicsObject = cEvt.other.GetBody().GetUserData();\r\n\t\t\t\r\n\t\t\tif (other is SalesWoman || other is Block || other is Cash)\r\n\t\t\t\t_fighting = false;\r\n\t\t}\r\n\t\t\r\n\t\tprotected function updateAnimation():void {\r\n\t\t\t\r\n\t\t\t_animation = _fighting ? \"attack\" : \"walk\";\r\n\t\t}\r\n\t}\r\n}<\/pre>\n<p>Now the bags. They are just a missile with categoryBits & maskBits filters. However the CE is prebuilt as a platformer engine, it means that Missiles remove Box2D gravity this way :<\/p>\n<pre lang=\"actionscript3\">override public function update(timeDelta:Number):void {\r\n\r\n\tvar removeGravity:V2 = new V2();\r\n\tremoveGravity.subtract(_box2D.world.GetGravity());\r\n\tremoveGravity.multiplyN(body.GetMass());\r\n\t\r\n\t_body.ApplyForce(removeGravity, _body.GetWorldCenter());\r\n}<\/pre>\n<p>In our game we don't have gravity so we just remove this unnecessary & expensive calculation! And finally Missile are defined as bullet and use Continous Collision Detection, this is very expensive. Since our missiles aren't fast we don't need that.<\/p>\n<p>The Bag class :<\/p>\n<pre lang=\"actionscript3\" line=\"1\">package games.live4sales.weapons {\r\n\t\r\n\timport Box2DAS.Common.V2;\r\n\timport Box2DAS.Dynamics.ContactEvent;\r\n\r\n\timport com.citrusengine.objects.platformer.box2d.Missile;\r\n\timport com.citrusengine.physics.Box2DCollisionCategories;\r\n\r\n\t\/**\r\n\t * @author Aymeric\r\n\t *\/\r\n\tpublic class Bag extends Missile {\r\n\r\n\t\tpublic function Bag(name:String, params:Object = null) {\r\n\t\t\tsuper(name, params);\r\n\t\t}\r\n\t\t\r\n\t\toverride public function update(timeDelta:Number):void {\r\n\t\t\t\r\n\t\t\tif (!_exploded)\r\n\t\t\t\t_body.SetLinearVelocity(_velocity);\r\n\t\t\telse\r\n\t\t\t\t_body.SetLinearVelocity(new V2());\r\n\t\t\t\r\n\t\t\tif (x > 480)\r\n\t\t\t\tkill = true;\r\n\t\t}\r\n\t\t\t\r\n\t\toverride protected function handleBeginContact(cEvt:ContactEvent):void {\r\n\t\t\texplode();\r\n\t\t}\r\n\t\t\r\n\t\toverride protected function defineBody():void {\r\n\t\t\t\r\n\t\t\tsuper.defineBody();\r\n\t\t\t\r\n\t\t\t_bodyDef.bullet = false;\r\n\t\t\t_bodyDef.allowSleep = true;\r\n\t\t}\r\n\t\t\r\n\t\toverride protected function defineFixture():void {\r\n\t\t\t\r\n\t\t\tsuper.defineFixture();\r\n\r\n\t\t\t_fixtureDef.filter.categoryBits = Box2DCollisionCategories.Get(\"Level\");\r\n\t\t\t_fixtureDef.filter.maskBits = Box2DCollisionCategories.GetAllExcept(\"Level\");\r\n\t\t}\r\n\r\n\t}\r\n}<\/pre>\n<p>I'm pretty sure that Box2D ninja user will find other optimizations, please share them.<\/p>\n<p>The example is over now. Again the hardest part is to find good parameters & balance to have the best user experience. To do that you have to try your game again & again and when you think it is ok : let's an other guy try it... then restart your balancing!<\/p>\n<p>That was an other non platformer game example for the CE! Cheers!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Edit : Nape version One week after the Osmos demo, I offer a new game made with the Citrus Engine based on an other popular game : Plant vs. Zombies! If you have never played this game, try it now! This is my clone made with the CE working as a browser game, air application, &hellip; <a href=\"http:\/\/www.aymericlamboley.fr\/blog\/live4sales-a-plant-vs-zombies-clone\/\" class=\"more-link\">Continue reading <span class=\"screen-reader-text\">Live4Sales, a Plant vs Zombies clone<\/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,115,51,33,11,114,6],"tags":[119,15,27,50,34,26,126],"_links":{"self":[{"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/posts\/721"}],"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=721"}],"version-history":[{"count":14,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/posts\/721\/revisions"}],"predecessor-version":[{"id":1271,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/posts\/721\/revisions\/1271"}],"wp:attachment":[{"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/media?parent=721"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/categories?post=721"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/www.aymericlamboley.fr\/blog\/wp-json\/wp\/v2\/tags?post=721"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}