ผลต่างระหว่างรุ่นของ "01219245/cocos2d-js/Sprites2"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
(ไม่แสดง 42 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
แถว 27: | แถว 27: | ||
== The player and its movement == | == The player and its movement == | ||
− | You should start by creating a new Cocos-JS project. | + | |
+ | === Create a new project, clean up, and set up a Git repository === | ||
+ | You should start by downloading the template and cleaning up the original Hello World app. Follow the instruction from the [[01219245/cocos2d-js/Sprites#Getting_started|previous tutorial]]. | ||
+ | |||
+ | {{กล่องสี|#eeeeff| | ||
+ | '''If you use the full version of Cocos2d-JS or Cocos2d-x''', you should start by creating a new Cocos-JS project. | ||
cocos new -l js --no-native ชื่อโปรเจ็ค | cocos new -l js --no-native ชื่อโปรเจ็ค | ||
This will create an example HelloWorld project. You should clean up the example by following instructions outlined in [[01219245/cocos2d-js/Sprites#Let.27s_clean_up_HelloWorld_and_start_with_our_empty_game|Tutorial 100]]. | This will create an example HelloWorld project. You should clean up the example by following instructions outlined in [[01219245/cocos2d-js/Sprites#Let.27s_clean_up_HelloWorld_and_start_with_our_empty_game|Tutorial 100]]. | ||
+ | }} | ||
Then, create a git repository at the project directory. | Then, create a git repository at the project directory. | ||
แถว 44: | แถว 50: | ||
We shall create class <tt>Player</tt> as <tt>src/Player.js</tt>. | We shall create class <tt>Player</tt> as <tt>src/Player.js</tt>. | ||
+ | {{synfile|src/Player.js}} | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
var Player = cc.Sprite.extend({ | var Player = cc.Sprite.extend({ | ||
แถว 55: | แถว 62: | ||
We shall create the player in <tt>GameLayer.init</tt>. To do so add these lines: | We shall create the player in <tt>GameLayer.init</tt>. To do so add these lines: | ||
+ | {{synfile|src/GameLayer.js}} | ||
<syntaxhighlight lang="javascript"> | <syntaxhighlight lang="javascript"> | ||
this.player = new Player(); | this.player = new Player(); | ||
แถว 63: | แถว 71: | ||
Note that we use constants <tt>screenWidth</tt> and <tt>screenHight</tt> (which are 800 and 600, respectively). Don't forget to add this constant in <tt>main.js</tt>. | Note that we use constants <tt>screenWidth</tt> and <tt>screenHight</tt> (which are 800 and 600, respectively). Don't forget to add this constant in <tt>main.js</tt>. | ||
+ | |||
+ | {{synfile|main.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | |||
+ | var screenWidth = 800; // add these two constants | ||
+ | var screenHeight = 600; // | ||
+ | |||
+ | cc.game.onStart = function(){ | ||
+ | cc.view.adjustViewPort(true); | ||
+ | cc.view.setDesignResolutionSize(screenWidth, screenHeight, cc.ResolutionPolicy.SHOW_ALL); // use them here | ||
+ | // ... | ||
+ | }; | ||
+ | cc.game.run(); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Last step is to update configuration and resource files. | ||
+ | |||
+ | 1. We need to add <tt>src/Player.js</tt> to the jsList in <tt>project.json</tt>. | ||
+ | |||
+ | {{synfile|project.json}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | // ... | ||
+ | "jsList" : [ | ||
+ | "src/resource.js", | ||
+ | "src/GameLayer.js", | ||
+ | "src/Player.js" | ||
+ | ] | ||
+ | // ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | 2. Since we are using resource <tt>res/images/dot.png</tt>, we should preload it (so that we do not have to see empty screen when we start our program). Put the file name in <tt>src/resource.js</tt> (See more in section '''Technicalities: preloading of resources''', later in this page.) | ||
+ | |||
+ | {{synfile|src/resource.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | var res = { | ||
+ | dot_png: 'res/images/dot.png' | ||
+ | }; | ||
+ | // ... | ||
+ | </syntaxhighlight> | ||
Try to refresh the game. You should see your sprite in the middle of the screen. | Try to refresh the game. You should see your sprite in the middle of the screen. | ||
{{gitcomment|Commit your work.}} | {{gitcomment|Commit your work.}} | ||
+ | |||
+ | === Review of physics === | ||
+ | You might forget all these, but if you want objects in your game to look and act a bit like real objects, you might have to recall stuffs you learned from mechanics. | ||
+ | |||
+ | Let's look at the basics. An object has a position, its position changes if it has non-zero velocity. | ||
+ | |||
+ | How can you change the player's position? Put something like this in <tt>Player.update</tt>: | ||
+ | |||
+ | this.setPosition( x, y ); | ||
+ | |||
+ | If you want to apply the velocity, you can change the player position based on the velocity. | ||
+ | |||
+ | If there is an acceleration, the object's velocity also changes. The Sprite do not have <tt>velocity</tt> as its property, so we will add it. You can update the velocity based on the acceleration. | ||
+ | |||
+ | These properties (the position, the velocity, and the acceleration) all have directions. Sometimes, you see negative velocity; this means the object is moving in an opposite direction as the positive direction. We shall follow the standard co-ordinate system for Cocos2d, i.e., for the y-axis, we think of the direction as going upwards. | ||
+ | |||
+ | In Physics, everything is continuous. When writing games, we don't really need exact physics, so we can move objects in discrete steps. (In fact, method <tt>update</tt> is also called with parameter <tt>dt</tt>, the time period between this call and the last call, and you can use this to make your simulation more smooth.) | ||
+ | |||
+ | So the usual pseudo code for physics is as follows. | ||
+ | |||
+ | pos = pos + velocity; | ||
+ | velocity = velocity + acceleration | ||
+ | |||
+ | === Falling dot === | ||
+ | To simulate the player falls, we should maintain the player's current velocity, so that we can make it falls as close as the real object. | ||
+ | |||
+ | Let's add this line that initialize property <tt>vy</tt> in <tt>Player.ctor</tt>: | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | this.vy = 15; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | You may wonder why we put 15 here. It is just pure guess at this point. However, when you write games, you might want to try various possible values and pick the best one (i.e., the one that make the game fun). | ||
+ | |||
+ | The <tt>update</tt> method changes the player's position | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | update: function( dt ) { | ||
+ | var pos = this.getPosition(); | ||
+ | this.setPosition( new cc.Point( pos.x, pos.y + this.vy ) ); | ||
+ | this.vy += -1; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Note that we update <tt>this.vy</tt> at the end of update. The constant <tt>-1</tt> is the acceleration. The parameter <tt>dt</tt> represents delta time; we do not use it for now. | ||
+ | |||
+ | Try to run the program. You should see the player falling. | ||
+ | |||
+ | While our program works, don't just rush to commit right away. Let's try to get rid of the magic numbers first, by defining them explicitly. | ||
+ | |||
+ | Add these line at the end of <tt>Player.js</tt> | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | Player.G = -1; | ||
+ | Player.STARTING_VELOCITY = 15; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Then replace <tt>15</tt> and <tt>-1</tt> in the code with the appropriate constants. | ||
+ | |||
+ | {{gitcomment|When your program looks good, commit it}} | ||
+ | |||
+ | === Jumping dot === | ||
+ | |||
+ | Now, let's make the dot jumps. Let's add method <tt>Player.jump</tt> that set the velocity to some positive amount. | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | jump: function() { | ||
+ | this.vy = Player.JUMPING_VELOCITY; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Also, add this constant after the class is defined in the <tt>.extend</tt> block. | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | Player.JUMPING_VELOCITY = 15; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | To jump, we have to call <tt>player.jump()</tt> in an appropriate time. We will response to keyboard inputs. We shall follow the style we did in the last tutorial. | ||
+ | |||
+ | First, add these functions. Function addKeyboardHandler registers the event handlers: onKeyDown and onKeyUp. | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | addKeyboardHandlers: function() { | ||
+ | var self = this; | ||
+ | cc.eventManager.addListener({ | ||
+ | event: cc.EventListener.KEYBOARD, | ||
+ | onKeyPressed : function( keyCode, event ) { | ||
+ | self.onKeyDown( keyCode, event ); | ||
+ | }, | ||
+ | onKeyReleased: function( keyCode, event ) { | ||
+ | self.onKeyUp( keyCode, event ); | ||
+ | } | ||
+ | }, this); | ||
+ | }, | ||
+ | |||
+ | onKeyDown: function( keyCode, event ) { | ||
+ | }, | ||
+ | |||
+ | onKeyUp: function( keyCode, event ) { | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Then, call addKeyboardHandlers in GameLayer.init | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | init: function() { | ||
+ | // ... | ||
+ | this.addKeyboardHandlers(); | ||
+ | // ... | ||
+ | }, | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | We will jump in any key input, so we shall modify <tt>onKeyDown</tt> as follows. | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | onKeyDown: function( keyCode, event ) { | ||
+ | this.player.jump(); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | To test this increment, you will have to click on the game canvas, and then quickly hit on any key to get the dot jumping. Try a few times to see how the dot moves. You can adjust the jumping velocity to make the movement nice. | ||
+ | |||
+ | {{gitcomment|After a few trials to make sure your code works, please commit.}} | ||
+ | |||
+ | == Game states == | ||
+ | It won't be nice to have the user start clicking right away after the game loads. Therefore we shouldn't let the player move before the game actually starts. To do so, we will add a '''state''' to the game. How various objects interact depends on the game state. In this tutorial, we shall simply add a property <tt>state</tt> to <tt>GameLayer</tt>, and perform various events and <tt>update</tt> methods according to this <tt>state</tt>. We will learn a cleaner way later on. | ||
+ | |||
+ | For now, the game has 2 states: <tt>FRONT</tt> and <tt>STARTED</tt>. After the game loads, its state is <tt>FRONT</tt>. In this state, nothing moves, and after the user hit any keyboard, it changes its state to <tt>STARTED</tt> with the dot start to jump. The dot falls and jumps as usual in the <tt>STARTED</tt> state. | ||
+ | |||
+ | Let's add the constants for states after the class <tt>GameLayer</tt> is defined (i.e., at the end of <tt>extend</tt> call). | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | GameLayer.STATES = { | ||
+ | FRONT: 1, | ||
+ | STARTED: 2 | ||
+ | }; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | With this, we can refer to states as <tt>GameLayer.STATES.FRONT</tt> and <tt>GameLayer.STATES.STARTED</tt>. | ||
+ | |||
+ | === Initial state === | ||
+ | |||
+ | We let property <tt>state</tt> of <tt>GameLayer</tt> keeps the current game state. We initialize the state in <tt>GameLayer.init</tt>: | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | this.state = GameLayer.STATES.FRONT; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === State transition === | ||
+ | |||
+ | Our game changes its state when the user hits the keyboard, we rewrite method <tt>onKeyDown</tt> to | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | onKeyDown: function( keyCode, event ) { | ||
+ | if ( this.state == GameLayer.STATES.FRONT ) { | ||
+ | this.state = GameLayer.STATES.STARTED; | ||
+ | // <--- some code to tell the player to start falling (TO BE ADDED LATER) | ||
+ | } | ||
+ | if ( this.state == GameLayer.STATES.STARTED ) { | ||
+ | this.player.jump(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Note that in the code above, if the condition in the first <tt>if</tt> holds, you will also execute the body of the second <tt>if</tt>. Therefore, the two conditions are dependent. When the user first hits the key, we let the player starts and ''also'' jumps. However, if you don't look carefully, you might not notice the ''jump'' call because it is in the second <tt>if</tt>. | ||
+ | |||
+ | We might want to make it a bit clearly like this: | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | onKeyDown: function( keyCode, event ) { | ||
+ | if ( this.state == GameLayer.STATES.FRONT ) { | ||
+ | this.state = GameLayer.STATES.STARTED; | ||
+ | // <--- some code to tell the player to start falling (TO BE ADDED LATER) | ||
+ | this.player.jump(); | ||
+ | } else if ( this.state == GameLayer.STATES.STARTED ) { | ||
+ | this.player.jump(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Or, you may want to use <tt>switch</tt> statement. | ||
+ | |||
+ | === Player's state === | ||
+ | Now, the <tt>Player</tt> shouldn't fall until the game tells it to get started. We shall add a state to the <tt>Player</tt> as well. Initialize property <tt>started</tt> in <tt>Player.ctor</tt>: | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | this.started = false; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Add method <tt>start</tt> to update this state. | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | start: function() { | ||
+ | this.started = true; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Finally, we only perform position update when the player is started. | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | update: function( dt ) { | ||
+ | if ( this.started ) { | ||
+ | // ... old update code here | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | We shall call | ||
+ | |||
+ | this.player.start() // <-- this is the code to tell the player to start falling. | ||
+ | |||
+ | in the <tt>onKeyDown</tt> method in <tt>GameLayer</tt>. | ||
+ | |||
+ | {{gitcomment|Try to see if the game works as expected. Commit your work after you are done.}} | ||
+ | |||
+ | == The pillar pair == | ||
+ | In this section, we will implement a moving pair of opposing pillars. Let's call them a ''pillar pair''. Clearly, we will use one or more sprites to represent it. Before we think about how to implement it, let's think about what we want from this ''thing'': (1) we want to move the pillar pair and (2) we want to check if the dot hits the pillars. | ||
+ | |||
+ | There are basically two approaches for this. | ||
+ | |||
+ | 1. Use 1 sprite with transparent background in the middle. | ||
+ | 2. Use 2 sprites (one for the top pillar, another for the bottom pillar). | ||
+ | |||
+ | Both approaches are shown below. | ||
+ | |||
+ | [[Image:Sprites-pillarpair1.png]] | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Question:''' Can you think of the advantages and disadvantages for using each approach? Expand to see some of the possible advantages and disadvantages. | ||
+ | <div class="mw-collapsible-content"> | ||
+ | Using 1 sprite is easier to deal with. However, the space between two opposing pillars are fixed. Using 2 sprites gives flexibility but it might be hard to to deal with two objects. It might be even worse if we have to deal with many pillar pairs. | ||
+ | </div> | ||
+ | </div> | ||
+ | |||
+ | Here, we will try to get the best out of both approaches. We will use 2 sprites, but we shall combine them into one object. | ||
+ | |||
+ | [[Image:Sprites-pillarpair2.png]] | ||
+ | |||
+ | The blue object in the figure above contains two red sprite objects. Specifically, we shall create a <tt>cc.Node</tt> object that contains two sprites. | ||
+ | |||
+ | === Sprite images === | ||
+ | Draw two images for the top and bottom pillars. Since we can place each pillar at different height, we should make the images large enough so that the other end of the pillar is still outside the screen. The figure below shows the sprites and the screen; the red border shows the screen boundary. | ||
+ | |||
+ | [[Image:Clipped-pillars.png]] | ||
+ | |||
+ | So let's create two images <tt>pillar-top.png</tt> and <tt>pillar-bottom.png</tt>, each of width 80 pixels and height 600 pixels. Save them in directory <tt>res/images</tt>. | ||
+ | |||
+ | === Node class === | ||
+ | Let's create class <tt>PillarPair</tt> in <tt>src/PillarPair.js</tt>. In its constructor, we create two sprites from the images we have just created. Then we add the sprites as the child of this <tt>Node</tt>. | ||
+ | |||
+ | {{synfile|src/PillarPair.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | var PillarPair = cc.Node.extend({ | ||
+ | ctor: function() { | ||
+ | this._super(); | ||
+ | this.topPillar = cc.Sprite.create( 'res/images/pillar-top.png' ); | ||
+ | this.topPillar.setAnchorPoint( new cc.Point( 0.5, 0 ) ); | ||
+ | this.topPillar.setPosition( new cc.Point( 0, 100 ) ); | ||
+ | this.addChild( this.topPillar ); | ||
+ | |||
+ | this.bottomPillar = cc.Sprite.create( 'res/images/pillar-bottom.png' ); | ||
+ | this.bottomPillar.setAnchorPoint( new cc.Point( 0.5, 1 ) ); | ||
+ | this.bottomPillar.setPosition( new cc.Point( 0, -100 ) ); | ||
+ | this.addChild( this.bottomPillar ); | ||
+ | } | ||
+ | }); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | To put both sprites in the correct position relative to the position of the <tt>PillarPair</tt>, we set two properties of each sprite: the AnchorPoint and the Position. To understand this, consider the top pillar. We call | ||
+ | |||
+ | this.topPillar.setAnchorPoint( new cc.Point( 0.5, 0 ) ); | ||
+ | |||
+ | to say that when we talk about the position of the top pillar, that position is the middle point (0.5) of the bottom (0) of the image (see Figure below, blue co-ordinates). For anchor points, the co-ordinate of the bottom-left corner of the sprite is (0,0) and the top-right corner is (1,1). Position (0.5,0) is the middle point of the bottom of the sprite. | ||
+ | |||
+ | We then call | ||
+ | |||
+ | this.topPillar.setPosition( new cc.Point( 0, 100 ) ); | ||
+ | |||
+ | to place the top pillar at position (0, 100) relative to the position of the pillar: this means the top pillar is 100 pixels above the Node position (see Figure below, red co-ordinates). | ||
+ | |||
+ | [[Image:Top-pillar-relative.png]] | ||
+ | |||
+ | Let's see if the pillars appear. Add file <tt>src/PillarPair.js</tt> to '''jsList''' in <tt>project.json</tt>. Then in <tt>GameLayer</tt>, let's add the code that create a <tt>PillarPair</tt> in <tt>GameLayer.init</tt>. | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | this.pillarPair = new PillarPair(); | ||
+ | this.pillarPair.setPosition( new cc.Point( 700, 300 ) ); | ||
+ | this.addChild( this.pillarPair ); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | {{gitcomment|If the pillar pair appears, commit your change.}} | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Question:''' What is the width of the space between the top pillar and the bottom pillar? | ||
+ | <div class="mw-collapsible-content"> | ||
+ | 200 pixels. | ||
+ | </div> | ||
+ | </div> | ||
+ | |||
+ | === Moving the pillars === | ||
+ | Let's add the code that move the pillars, <tt>PillarPair.update</tt>. | ||
+ | |||
+ | {{synfile|src/PillarPair.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | update: function( dt ) { | ||
+ | this.setPositionX( this.getPositionX() - 5 ); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Then in <tt>GameLayer.init</tt>, add a line | ||
+ | |||
+ | {{synfile|GameLayer.js}} | ||
+ | |||
+ | this.pillarPair.scheduleUpdate(); | ||
+ | |||
+ | after the <tt>PillarPair</tt> is created. | ||
+ | |||
+ | {{gitcomment|Don't forget to commit.}} | ||
+ | |||
+ | === Technicalities: preloading of resources === | ||
+ | If you runs the game a few times, you might notice that sometimes some pillar does not appear. This is because when we create the sprites, the images we want to use are not completely loaded. To avoid this problem, we will tell the Cocos2d-JS library to load all our resources before starting our <tt>Scene</tt>. This is done by specifying resources in src/resource.js | ||
+ | |||
+ | {{synfile|src/resource.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | // Add files in res | ||
+ | var res = { | ||
+ | dot_png: 'res/images/dot.png', | ||
+ | pillar_top_png: 'res/images/pillar-top.png', | ||
+ | pillar_bottom_png: 'res/images/pillar-bottom.png' | ||
+ | }; | ||
+ | |||
+ | // Leave the original code for assigning g_resources untouched. | ||
+ | var g_resources = []; | ||
+ | for (var i in res) { | ||
+ | g_resources.push(res[i]); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Note that this file lists all images and probably other resources (e.g., sound files) that we want to use. | ||
+ | |||
+ | Now, when you start our game, you'll see the Cocos2d-html5 preloading start screen. | ||
+ | |||
+ | {{gitcomment|If your code works, commit the changes.}} | ||
+ | |||
+ | === Moving the pillars after the game starts === | ||
+ | Currently, we create the pillars right after the scene starts. We should synchronize this with the game state. | ||
+ | |||
+ | We will create the pillar pair often, so let's take the code for pillar creation out of <tt>GameLayer.init</tt> and place it in method <tt>createPillarPair</tt>: | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | createPillarPair: function() { | ||
+ | this.pillarPair = new PillarPair(); | ||
+ | this.pillarPair.setPosition( new cc.Point( 900, 300 ) ); | ||
+ | this.addChild( this.pillarPair ); | ||
+ | this.pillarPair.scheduleUpdate(); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | In <tt>GameLayer.init</tt>, let's put this null initialization in place of the old pillar pair creation. | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | |||
+ | this.pillarPair = null; | ||
+ | |||
+ | Finally, let's call <tt>createPillarPair</tt> after the game state changes to <tt>GameLayer.STATES.STARTED</tt> in <tt>onKeyDown</tt>: | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | if ( this.state == GameLayer.STATES.FRONT ) { | ||
+ | this.state = GameLayer.STATES.STARTED; | ||
+ | this.createPillarPair(); | ||
+ | this.player.start(); | ||
+ | this.player.jump(); | ||
+ | } else if ( ... ) { | ||
+ | // ... | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Try the game to see if the pillar pair starts moving after we hit a keyboard. | ||
+ | |||
+ | {{gitcomment|Commit your change.}} | ||
+ | |||
+ | The code for starting a new game currently lies in an event handler <tt>onKeyDown</tt>. This method will get longer and longer as we work on our game. So let's clean the method a bit. We shall extract the game initialization code into method <tt>startGame</tt>. | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | onKeyDown: function( keyCode, event ) { | ||
+ | if ( this.state == GameLayer.STATES.FRONT ) { | ||
+ | this.startGame(); | ||
+ | this.state = GameLayer.STATES.STARTED; | ||
+ | } else if ( ... ) { | ||
+ | // ... | ||
+ | } | ||
+ | }, | ||
+ | |||
+ | // ... | ||
+ | |||
+ | startGame: function() { | ||
+ | this.createPillarPair(); | ||
+ | this.player.start(); | ||
+ | this.player.jump(); | ||
+ | }, | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Test the code, and commit. | ||
+ | |||
+ | {{gitcomment|Commit your change.}} | ||
+ | |||
+ | === z-axis: Showing the player in front of the pillars === | ||
+ | If you move the player through the pillars, you should notice that the pillars are drawn over the player. It would be better to have the player in front of the pillars. To do so, we assign the higher z co-ordinate to the player when we add the player to the <tt>GameLayer</tt> with method <tt>addChild</tt>. | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | this.player = new Player(); | ||
+ | this.player.setPosition( new cc.Point( screenWidth / 2, screenHeight / 2 ) ); | ||
+ | this.addChild( this.player, 1 ); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Note that the default z co-ordinate is 0; so the z-co-ordinates of the pillars is lower than the player's. | ||
+ | |||
+ | {{gitcomment|Test and commit the changes.}} | ||
+ | |||
+ | === Exercise: Reusing the pillars === | ||
+ | In this game, our player will have to fly passing a lot of pillar pairs. However, we will not always create a new pillar pair. Instead, we shall reuse the old pillar pair that recently disappear on the left side of the screen. | ||
+ | |||
+ | '''EXERCISE:''' modify method <tt>PillarPair.update</tt> so that right after the pillar pair move outside the screen, it re-enter at the right side of the screen. | ||
+ | |||
+ | {{gitcomment|Test and commit the changes.}} | ||
+ | |||
+ | == Exercise: Collision detection == | ||
+ | We will check if our dot hits the pillar pair. We will write method <tt>hit</tt> in class <tt>PillarPair</tt>: | ||
+ | |||
+ | {{synfile|src/PillarPair.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | hit: function( player ) { | ||
+ | // return true if the player hits the pillar pair. | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Unit testing === | ||
+ | |||
+ | <div class="toccolours mw-collapsible mw-collapsed"> | ||
+ | '''Question:''' When we write method <tt>hit</tt>, how can we test if the method is correct? | ||
+ | <div class="mw-collapsible-content"> | ||
+ | I'll have to test it. Right now, we have to run the game and try to hit and miss the pillars in as many cases as possible. This is rather hard to do because we need to control the dot as well. This make conventional testing for this logic hard to do. | ||
+ | </div> | ||
+ | </div> | ||
+ | |||
+ | We will try to use automated testing for this part of the game. The idea is simple: we will try to explicitly call <tt>hit</tt> in various ways to check if it returns the correct decision. | ||
+ | |||
+ | For normal functions, this is not very hard to do. However, in this case, it is fairly hard to directly call <tt>hit</tt> because it is a method in class derived from <tt>cc.Node</tt> for which we do not quite know how to create it outside the Cocos2d-html framework. | ||
+ | |||
+ | We will get away with that by extracting the decision function as a stand-alone function and test it independently of the Cocos2d-JS framework. Let's finish method <tt>hit</tt> first. | ||
+ | |||
+ | {{synfile|src/PillarPair.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | hit: function( player ) { | ||
+ | var playerPos = player.getPosition(); | ||
+ | var myPos = this.getPosition(); | ||
+ | |||
+ | return checkPlayerPillarCollision( playerPos.x, playerPos.y, myPos.x, myPos.y ); | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | We will write function <tt>checkPlayerPillarCollision</tt> in a separated file, so that we can test it without having to deal with Cocos2d-html5 library. | ||
+ | |||
+ | Since this part is quite a detour, I'll give more detailed instructions. | ||
+ | |||
+ | Let's create a (broken) function <tt>checkPlayerPillarCollision</tt> in file <tt>src/coldetect.js</tt>. | ||
+ | |||
+ | {{synfile|src/coldetect.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | var checkPlayerPillarCollision = function( playerX, playerY, pillarX, pillarY ) { | ||
+ | return false; | ||
+ | }; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Then, add this file (<tt>src/coldetect.js</tt>) to the <tt>jsFiles</tt> config in <tt>project.json</tt>. (We might want to do this after we have finish writing the function, but let's do it now so that we won't forget.) | ||
+ | |||
+ | We will test this function in a separated main JavaScript program. We will put all these testing codes in a separated directory <tt>src/test</tt>. | ||
+ | |||
+ | Let's create directory <tt>src/test</tt>. | ||
+ | |||
+ | Add <tt>index.html</tt> in <tt>src/test</tt>: | ||
+ | |||
+ | {{synfile|src/test/index.html}} | ||
+ | <syntaxhighlight lang="html5"> | ||
+ | <!doctype html> | ||
+ | <html lang="en"> | ||
+ | <body> | ||
+ | <ul id="test-output"> | ||
+ | </ul> | ||
+ | <script src="../coldetect.js"></script> | ||
+ | <script src="test.js"></script> | ||
+ | </body> | ||
+ | </html> | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Our main JavaScript program that runs the unit testing will be in <tt>test.js</tt>. Create <tt>test.js</tt> in directory <tt>src/test</tt>: | ||
+ | |||
+ | {{synfile|src/test/test.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | var assert = function( actual, expected, message ) { | ||
+ | var msg = ""; | ||
+ | |||
+ | if ( actual === expected ) { | ||
+ | msg = '<span style="color: green">OK</span>'; | ||
+ | } else { | ||
+ | msg = '<span style="color: red; font-weight: bold">Failed:</span> ' + message; | ||
+ | } | ||
+ | |||
+ | var elt = document.getElementById( 'test-output' ); | ||
+ | elt.innerHTML += "<li>" + msg + "</li>"; | ||
+ | }; | ||
+ | |||
+ | assert( checkPlayerPillarCollision( 100, 100, 300, 200 ), false, | ||
+ | 'when the dot is very far left of the pillar pair' ); | ||
+ | assert( checkPlayerPillarCollision( 300, 300, 300, 200 ), true, | ||
+ | 'when the dot hit the middle of the top pillar' ); | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | This <tt>test.js</tT> file contains function <tt>assert</tt> and a few lines of test cases. You can ignore function assert for now, but these two calls to <tt>assert</tt> checks the following two collision cases: | ||
+ | |||
+ | [[Image:Test-coldetect-testcases.png]] | ||
+ | |||
+ | Let's access the test page that we have just added. Go to URL like: | ||
+ | |||
+ | file:///XXXXX/mygames/flappydot/src/test/ | ||
+ | |||
+ | (I.e., add <tt>/src/test/</tt> after the URL for the game.) You should see something like this: | ||
+ | |||
+ | [[Image:Test-coldetect.png]] | ||
+ | |||
+ | If you look at <tt>test.js</tt>, each <tt>assert</tt> line is a single test case. Currently our function is broken int the second test case. You can start fixing the function in <tt>src/coldetect.js</tt> to make sure that you get all "OK"'s. | ||
+ | |||
+ | '''NOTES:''' You '''don't''' have to make the collision detection perfect to make the game fun. For example, you don't have to think of the dot as a perfect shape that you have drawn or even a circle, but you can think of it as a simple rectangle which is large enough. Our dot is of size 40 x 40, but we have some curve. Therefore we can view it as a rectangle of size, say, 30 x 30 centered at the position of the player. | ||
+ | |||
+ | Clearly these two test cases are not enough to ensure that your <tt>checkPlayerPillarCollision</tt> works correctly. Therefore, after your code for <tt>checkPlayerPillarCollision</tt> passes these two test cases, you might want to add more test cases. The figure below shows a few other test cases that you should try adding to the test script. | ||
+ | |||
+ | [[Image:Test-coldetect-moretests.png]] | ||
+ | |||
+ | {{gitcomment|Don't forget to commit your work at this point.}} | ||
+ | |||
+ | === Combining with the game mechanics === | ||
+ | |||
+ | With method <tt>PillarPair.hit</tt>, we can regularly checks if the player hits the pillars. We create method <tt>update</tt> in <tt>GameLayer</tt> to do this. | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | update: function( dt ) { | ||
+ | if ( this.state == GameLayer.STATES.STARTED ) { | ||
+ | if ( this.pillarPair && this.pillarPair.hit( this.player ) ) { | ||
+ | // ... do something | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Note that we only check collision when the game is started. To get this method <tt>update</tt> running, don't forget to call | ||
+ | |||
+ | this.scheduleUpdate(); | ||
+ | |||
+ | in <tt>GameLayer.init</tt>. | ||
+ | |||
+ | What should we do when the player hits a pillar? | ||
+ | |||
+ | First, the game state should change. Let's add another state constant: | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | GameLayer.STATES = { | ||
+ | FRONT: 1, | ||
+ | STARTED: 2, | ||
+ | DEAD: 3 | ||
+ | }; | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Also, we should stop the player and the pillars from moving. Let's write method <tt>endGame</tt> in <tt>GameLayer</tt> to do this: | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | endGame: function() { | ||
+ | this.player.stop(); | ||
+ | if ( this.pillarPair ) { | ||
+ | this.pillarPair.unscheduleUpdate(); | ||
+ | } | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | and add method <tt>Player.stop</tt> to class <tt>Player</tt>: | ||
+ | |||
+ | {{synfile|src/Player.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | stop: function() { | ||
+ | this.started = false; | ||
+ | } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | With this, the <tt>update</tt> method becomes: | ||
+ | |||
+ | {{synfile|src/GameLayer.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | update: function( dt ) { | ||
+ | if ( this.state == GameLayer.STATES.STARTED ) { | ||
+ | if ( this.pillarPair && this.pillarPair.hit( this.player ) ) { | ||
+ | this.endGame(); | ||
+ | this.state = GameLayer.STATES.DEAD; | ||
+ | } | ||
+ | } | ||
+ | }, | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Try the game. You might want to try a few hit/miss at the pillars to manually test the collision detection function as well. | ||
+ | |||
+ | {{gitcomment|The game should be somewhat playable. Don't forget to commit your work.}} | ||
+ | |||
+ | == Exercise: Random the pillar heights == | ||
+ | Let's create the pillar pairs with different heights. Our screen's height is 600 pixels, and we probably do not want the pillar to be too high or too low. | ||
+ | |||
+ | '''Exerciese:''' Add this method to <tt>PillarPair</tt>. | ||
+ | |||
+ | {{synfile|src/PillarPair.js}} | ||
+ | <syntaxhighlight lang="javascript"> | ||
+ | randomPositionY: function() { | ||
+ | // ... change the y position of the pillar pair. | ||
+ | }, | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Don't forget to call this method when (1) you create a pillar pair, and (2) when the piilar pair wraps around the screen. | ||
+ | |||
+ | '''Notes:''' When you test this method, you may want to switch off the collision detection. This can be easily done by adding <tt>return false;</tt> at the top of <tt>checkPlayerPillarCollision</tt> function. When you are done, don't forget to remove this hack. | ||
+ | |||
+ | {{gitcomment|Commit your work. Don't forget to remove the hack to avoid <tt>checkPlayerPillarCollision</tt>.}} | ||
+ | |||
+ | == Exercise: The series of pillar pairs == | ||
+ | As we have most objects written nicely, adding more pillars can be done by creating more <tt>PillarPair</tt>'s and spread them out nicely across the screen. | ||
+ | |||
+ | We only outline the changes that you need to make. It is your exercises to get all these steps done. | ||
+ | |||
+ | * '''PillarPair creation:''' Right now we only create one pillar pair. We should create more and keep them in an array. Make sure you spread them out nicely over the x co-ordinate. Don't forget to rename the method to <tt>createPillars</tt> (with the '''s'''). | ||
+ | * '''Collision detection:''' We can't just call <tt>this.pillarPair.hit( ... )</tt>. It would be nice to write a method for checking collision on all pillar pairs. | ||
+ | * '''<tt>endGame</tt>:''' Now we have to stop all the pillars. | ||
+ | |||
+ | {{gitcomment|For each of these step, don't forget to commit when you get a unit of work done.}} | ||
+ | |||
+ | == Additional Exercises == | ||
+ | There are many ways to make the game more fun like a real game. Here we list a few: | ||
+ | |||
+ | * After the player dies, the game stops. Implement the game restart feature, i.e., when the player hit any key after the player dies, the game should restart. | ||
+ | * The game shows no score. Add the scoring to the game. | ||
+ | * Flappy dot is extremely hard because you can hit only once. Add more lives to the player. | ||
+ | * In our game, the pillar pair can have different spacing. Implement this feature to make the game extremely hard. | ||
+ | * You can allow the dot to fire a few rockets to destroy the pillars. | ||
+ | |||
+ | Good luck. Have fun writing games! | ||
+ | |||
+ | {{gitcomment|Don't forget to regularly commit your work.}} |
รุ่นแก้ไขปัจจุบันเมื่อ 04:49, 1 มีนาคม 2559
- This is part of 01219245, 2nd semester 2557.
In this tutorial, we will recreate a clone of a wonderful Flappy Bird. Let's call it Flappy Dot (as our player would look like a dot). We will develop basic game mechanics in this tutorial. We will try to add special effects to the game in the next tutorial.
- Make sure you have completed Tutorial 100 on basic sprites.
เนื้อหา
Task breakdown
Before we start, make sure you know how this game works. You may want to try it for a bit. I guess many of your friends have it on their phones. This is how our game would look like:
As usual, let's start by thinking about the possible list of increments we would add to an empty project to get this game.
When you get your list, please see the steps that we plan to take here.
- Show the player on the screen.
- The player can jump and fall. (Implement player physics)
- Show a single pillar pair.
- Move the pillar pair across the screen.
- Let the pillar pair reappear.
- Check for player-pillar collision.
- Make the game with one pillar pair.
- Show more than one pillar pairs.
The player and its movement
Create a new project, clean up, and set up a Git repository
You should start by downloading the template and cleaning up the original Hello World app. Follow the instruction from the previous tutorial.
If you use the full version of Cocos2d-JS or Cocos2d-x, you should start by creating a new Cocos-JS project.
cocos new -l js --no-native ชื่อโปรเจ็ค
This will create an example HelloWorld project. You should clean up the example by following instructions outlined in Tutorial 100.
Then, create a git repository at the project directory.
Creating the sprite
In this step, we shall create a sprite for the player, and show it in the middle of the screen.
Use a graphic editor to create an image for our player. The image should be of size 40 pixels x 40 pixels. Save the image as res/images/dot.png and try to make it look cute.
We shall create class Player as src/Player.js.
var Player = cc.Sprite.extend({
ctor: function() {
this._super();
this.initWithFile( 'res/images/dot.png' );
}
});
We shall create the player in GameLayer.init. To do so add these lines:
this.player = new Player();
this.player.setPosition( new cc.Point( screenWidth / 2, screenHeight / 2 ) );
this.addChild( this.player );
this.player.scheduleUpdate();
Note that we use constants screenWidth and screenHight (which are 800 and 600, respectively). Don't forget to add this constant in main.js.
var screenWidth = 800; // add these two constants
var screenHeight = 600; //
cc.game.onStart = function(){
cc.view.adjustViewPort(true);
cc.view.setDesignResolutionSize(screenWidth, screenHeight, cc.ResolutionPolicy.SHOW_ALL); // use them here
// ...
};
cc.game.run();
Last step is to update configuration and resource files.
1. We need to add src/Player.js to the jsList in project.json.
// ...
"jsList" : [
"src/resource.js",
"src/GameLayer.js",
"src/Player.js"
]
// ...
2. Since we are using resource res/images/dot.png, we should preload it (so that we do not have to see empty screen when we start our program). Put the file name in src/resource.js (See more in section Technicalities: preloading of resources, later in this page.)
var res = {
dot_png: 'res/images/dot.png'
};
// ...
Try to refresh the game. You should see your sprite in the middle of the screen.
Review of physics
You might forget all these, but if you want objects in your game to look and act a bit like real objects, you might have to recall stuffs you learned from mechanics.
Let's look at the basics. An object has a position, its position changes if it has non-zero velocity.
How can you change the player's position? Put something like this in Player.update:
this.setPosition( x, y );
If you want to apply the velocity, you can change the player position based on the velocity.
If there is an acceleration, the object's velocity also changes. The Sprite do not have velocity as its property, so we will add it. You can update the velocity based on the acceleration.
These properties (the position, the velocity, and the acceleration) all have directions. Sometimes, you see negative velocity; this means the object is moving in an opposite direction as the positive direction. We shall follow the standard co-ordinate system for Cocos2d, i.e., for the y-axis, we think of the direction as going upwards.
In Physics, everything is continuous. When writing games, we don't really need exact physics, so we can move objects in discrete steps. (In fact, method update is also called with parameter dt, the time period between this call and the last call, and you can use this to make your simulation more smooth.)
So the usual pseudo code for physics is as follows.
pos = pos + velocity; velocity = velocity + acceleration
Falling dot
To simulate the player falls, we should maintain the player's current velocity, so that we can make it falls as close as the real object.
Let's add this line that initialize property vy in Player.ctor:
this.vy = 15;
You may wonder why we put 15 here. It is just pure guess at this point. However, when you write games, you might want to try various possible values and pick the best one (i.e., the one that make the game fun).
The update method changes the player's position
update: function( dt ) {
var pos = this.getPosition();
this.setPosition( new cc.Point( pos.x, pos.y + this.vy ) );
this.vy += -1;
}
Note that we update this.vy at the end of update. The constant -1 is the acceleration. The parameter dt represents delta time; we do not use it for now.
Try to run the program. You should see the player falling.
While our program works, don't just rush to commit right away. Let's try to get rid of the magic numbers first, by defining them explicitly.
Add these line at the end of Player.js
Player.G = -1;
Player.STARTING_VELOCITY = 15;
Then replace 15 and -1 in the code with the appropriate constants.
Jumping dot
Now, let's make the dot jumps. Let's add method Player.jump that set the velocity to some positive amount.
jump: function() {
this.vy = Player.JUMPING_VELOCITY;
}
Also, add this constant after the class is defined in the .extend block.
Player.JUMPING_VELOCITY = 15;
To jump, we have to call player.jump() in an appropriate time. We will response to keyboard inputs. We shall follow the style we did in the last tutorial.
First, add these functions. Function addKeyboardHandler registers the event handlers: onKeyDown and onKeyUp.
addKeyboardHandlers: function() {
var self = this;
cc.eventManager.addListener({
event: cc.EventListener.KEYBOARD,
onKeyPressed : function( keyCode, event ) {
self.onKeyDown( keyCode, event );
},
onKeyReleased: function( keyCode, event ) {
self.onKeyUp( keyCode, event );
}
}, this);
},
onKeyDown: function( keyCode, event ) {
},
onKeyUp: function( keyCode, event ) {
}
Then, call addKeyboardHandlers in GameLayer.init
init: function() {
// ...
this.addKeyboardHandlers();
// ...
},
We will jump in any key input, so we shall modify onKeyDown as follows.
onKeyDown: function( keyCode, event ) {
this.player.jump();
}
To test this increment, you will have to click on the game canvas, and then quickly hit on any key to get the dot jumping. Try a few times to see how the dot moves. You can adjust the jumping velocity to make the movement nice.
Game states
It won't be nice to have the user start clicking right away after the game loads. Therefore we shouldn't let the player move before the game actually starts. To do so, we will add a state to the game. How various objects interact depends on the game state. In this tutorial, we shall simply add a property state to GameLayer, and perform various events and update methods according to this state. We will learn a cleaner way later on.
For now, the game has 2 states: FRONT and STARTED. After the game loads, its state is FRONT. In this state, nothing moves, and after the user hit any keyboard, it changes its state to STARTED with the dot start to jump. The dot falls and jumps as usual in the STARTED state.
Let's add the constants for states after the class GameLayer is defined (i.e., at the end of extend call).
GameLayer.STATES = {
FRONT: 1,
STARTED: 2
};
With this, we can refer to states as GameLayer.STATES.FRONT and GameLayer.STATES.STARTED.
Initial state
We let property state of GameLayer keeps the current game state. We initialize the state in GameLayer.init:
this.state = GameLayer.STATES.FRONT;
State transition
Our game changes its state when the user hits the keyboard, we rewrite method onKeyDown to
onKeyDown: function( keyCode, event ) {
if ( this.state == GameLayer.STATES.FRONT ) {
this.state = GameLayer.STATES.STARTED;
// <--- some code to tell the player to start falling (TO BE ADDED LATER)
}
if ( this.state == GameLayer.STATES.STARTED ) {
this.player.jump();
}
}
Note that in the code above, if the condition in the first if holds, you will also execute the body of the second if. Therefore, the two conditions are dependent. When the user first hits the key, we let the player starts and also jumps. However, if you don't look carefully, you might not notice the jump call because it is in the second if.
We might want to make it a bit clearly like this:
onKeyDown: function( keyCode, event ) {
if ( this.state == GameLayer.STATES.FRONT ) {
this.state = GameLayer.STATES.STARTED;
// <--- some code to tell the player to start falling (TO BE ADDED LATER)
this.player.jump();
} else if ( this.state == GameLayer.STATES.STARTED ) {
this.player.jump();
}
}
Or, you may want to use switch statement.
Player's state
Now, the Player shouldn't fall until the game tells it to get started. We shall add a state to the Player as well. Initialize property started in Player.ctor:
this.started = false;
Add method start to update this state.
start: function() {
this.started = true;
}
Finally, we only perform position update when the player is started.
update: function( dt ) {
if ( this.started ) {
// ... old update code here
}
}
We shall call
this.player.start() // <-- this is the code to tell the player to start falling.
in the onKeyDown method in GameLayer.
The pillar pair
In this section, we will implement a moving pair of opposing pillars. Let's call them a pillar pair. Clearly, we will use one or more sprites to represent it. Before we think about how to implement it, let's think about what we want from this thing: (1) we want to move the pillar pair and (2) we want to check if the dot hits the pillars.
There are basically two approaches for this.
1. Use 1 sprite with transparent background in the middle. 2. Use 2 sprites (one for the top pillar, another for the bottom pillar).
Both approaches are shown below.
Question: Can you think of the advantages and disadvantages for using each approach? Expand to see some of the possible advantages and disadvantages.
Using 1 sprite is easier to deal with. However, the space between two opposing pillars are fixed. Using 2 sprites gives flexibility but it might be hard to to deal with two objects. It might be even worse if we have to deal with many pillar pairs.
Here, we will try to get the best out of both approaches. We will use 2 sprites, but we shall combine them into one object.
The blue object in the figure above contains two red sprite objects. Specifically, we shall create a cc.Node object that contains two sprites.
Sprite images
Draw two images for the top and bottom pillars. Since we can place each pillar at different height, we should make the images large enough so that the other end of the pillar is still outside the screen. The figure below shows the sprites and the screen; the red border shows the screen boundary.
So let's create two images pillar-top.png and pillar-bottom.png, each of width 80 pixels and height 600 pixels. Save them in directory res/images.
Node class
Let's create class PillarPair in src/PillarPair.js. In its constructor, we create two sprites from the images we have just created. Then we add the sprites as the child of this Node.
var PillarPair = cc.Node.extend({
ctor: function() {
this._super();
this.topPillar = cc.Sprite.create( 'res/images/pillar-top.png' );
this.topPillar.setAnchorPoint( new cc.Point( 0.5, 0 ) );
this.topPillar.setPosition( new cc.Point( 0, 100 ) );
this.addChild( this.topPillar );
this.bottomPillar = cc.Sprite.create( 'res/images/pillar-bottom.png' );
this.bottomPillar.setAnchorPoint( new cc.Point( 0.5, 1 ) );
this.bottomPillar.setPosition( new cc.Point( 0, -100 ) );
this.addChild( this.bottomPillar );
}
});
To put both sprites in the correct position relative to the position of the PillarPair, we set two properties of each sprite: the AnchorPoint and the Position. To understand this, consider the top pillar. We call
this.topPillar.setAnchorPoint( new cc.Point( 0.5, 0 ) );
to say that when we talk about the position of the top pillar, that position is the middle point (0.5) of the bottom (0) of the image (see Figure below, blue co-ordinates). For anchor points, the co-ordinate of the bottom-left corner of the sprite is (0,0) and the top-right corner is (1,1). Position (0.5,0) is the middle point of the bottom of the sprite.
We then call
this.topPillar.setPosition( new cc.Point( 0, 100 ) );
to place the top pillar at position (0, 100) relative to the position of the pillar: this means the top pillar is 100 pixels above the Node position (see Figure below, red co-ordinates).
Let's see if the pillars appear. Add file src/PillarPair.js to jsList in project.json. Then in GameLayer, let's add the code that create a PillarPair in GameLayer.init.
this.pillarPair = new PillarPair();
this.pillarPair.setPosition( new cc.Point( 700, 300 ) );
this.addChild( this.pillarPair );
Question: What is the width of the space between the top pillar and the bottom pillar?
200 pixels.
Moving the pillars
Let's add the code that move the pillars, PillarPair.update.
update: function( dt ) {
this.setPositionX( this.getPositionX() - 5 );
}
Then in GameLayer.init, add a line
this.pillarPair.scheduleUpdate();
after the PillarPair is created.
Technicalities: preloading of resources
If you runs the game a few times, you might notice that sometimes some pillar does not appear. This is because when we create the sprites, the images we want to use are not completely loaded. To avoid this problem, we will tell the Cocos2d-JS library to load all our resources before starting our Scene. This is done by specifying resources in src/resource.js
// Add files in res
var res = {
dot_png: 'res/images/dot.png',
pillar_top_png: 'res/images/pillar-top.png',
pillar_bottom_png: 'res/images/pillar-bottom.png'
};
// Leave the original code for assigning g_resources untouched.
var g_resources = [];
for (var i in res) {
g_resources.push(res[i]);
}
Note that this file lists all images and probably other resources (e.g., sound files) that we want to use.
Now, when you start our game, you'll see the Cocos2d-html5 preloading start screen.
Moving the pillars after the game starts
Currently, we create the pillars right after the scene starts. We should synchronize this with the game state.
We will create the pillar pair often, so let's take the code for pillar creation out of GameLayer.init and place it in method createPillarPair:
createPillarPair: function() {
this.pillarPair = new PillarPair();
this.pillarPair.setPosition( new cc.Point( 900, 300 ) );
this.addChild( this.pillarPair );
this.pillarPair.scheduleUpdate();
}
In GameLayer.init, let's put this null initialization in place of the old pillar pair creation.
this.pillarPair = null;
Finally, let's call createPillarPair after the game state changes to GameLayer.STATES.STARTED in onKeyDown:
if ( this.state == GameLayer.STATES.FRONT ) {
this.state = GameLayer.STATES.STARTED;
this.createPillarPair();
this.player.start();
this.player.jump();
} else if ( ... ) {
// ...
}
Try the game to see if the pillar pair starts moving after we hit a keyboard.
The code for starting a new game currently lies in an event handler onKeyDown. This method will get longer and longer as we work on our game. So let's clean the method a bit. We shall extract the game initialization code into method startGame.
onKeyDown: function( keyCode, event ) {
if ( this.state == GameLayer.STATES.FRONT ) {
this.startGame();
this.state = GameLayer.STATES.STARTED;
} else if ( ... ) {
// ...
}
},
// ...
startGame: function() {
this.createPillarPair();
this.player.start();
this.player.jump();
},
Test the code, and commit.
z-axis: Showing the player in front of the pillars
If you move the player through the pillars, you should notice that the pillars are drawn over the player. It would be better to have the player in front of the pillars. To do so, we assign the higher z co-ordinate to the player when we add the player to the GameLayer with method addChild.
this.player = new Player();
this.player.setPosition( new cc.Point( screenWidth / 2, screenHeight / 2 ) );
this.addChild( this.player, 1 );
Note that the default z co-ordinate is 0; so the z-co-ordinates of the pillars is lower than the player's.
Exercise: Reusing the pillars
In this game, our player will have to fly passing a lot of pillar pairs. However, we will not always create a new pillar pair. Instead, we shall reuse the old pillar pair that recently disappear on the left side of the screen.
EXERCISE: modify method PillarPair.update so that right after the pillar pair move outside the screen, it re-enter at the right side of the screen.
Exercise: Collision detection
We will check if our dot hits the pillar pair. We will write method hit in class PillarPair:
hit: function( player ) {
// return true if the player hits the pillar pair.
}
Unit testing
Question: When we write method hit, how can we test if the method is correct?
I'll have to test it. Right now, we have to run the game and try to hit and miss the pillars in as many cases as possible. This is rather hard to do because we need to control the dot as well. This make conventional testing for this logic hard to do.
We will try to use automated testing for this part of the game. The idea is simple: we will try to explicitly call hit in various ways to check if it returns the correct decision.
For normal functions, this is not very hard to do. However, in this case, it is fairly hard to directly call hit because it is a method in class derived from cc.Node for which we do not quite know how to create it outside the Cocos2d-html framework.
We will get away with that by extracting the decision function as a stand-alone function and test it independently of the Cocos2d-JS framework. Let's finish method hit first.
hit: function( player ) {
var playerPos = player.getPosition();
var myPos = this.getPosition();
return checkPlayerPillarCollision( playerPos.x, playerPos.y, myPos.x, myPos.y );
}
We will write function checkPlayerPillarCollision in a separated file, so that we can test it without having to deal with Cocos2d-html5 library.
Since this part is quite a detour, I'll give more detailed instructions.
Let's create a (broken) function checkPlayerPillarCollision in file src/coldetect.js.
var checkPlayerPillarCollision = function( playerX, playerY, pillarX, pillarY ) {
return false;
};
Then, add this file (src/coldetect.js) to the jsFiles config in project.json. (We might want to do this after we have finish writing the function, but let's do it now so that we won't forget.)
We will test this function in a separated main JavaScript program. We will put all these testing codes in a separated directory src/test.
Let's create directory src/test.
Add index.html in src/test:
<!doctype html>
<html lang="en">
<body>
<ul id="test-output">
</ul>
<script src="../coldetect.js"></script>
<script src="test.js"></script>
</body>
</html>
Our main JavaScript program that runs the unit testing will be in test.js. Create test.js in directory src/test:
var assert = function( actual, expected, message ) {
var msg = "";
if ( actual === expected ) {
msg = '<span style="color: green">OK</span>';
} else {
msg = '<span style="color: red; font-weight: bold">Failed:</span> ' + message;
}
var elt = document.getElementById( 'test-output' );
elt.innerHTML += "<li>" + msg + "</li>";
};
assert( checkPlayerPillarCollision( 100, 100, 300, 200 ), false,
'when the dot is very far left of the pillar pair' );
assert( checkPlayerPillarCollision( 300, 300, 300, 200 ), true,
'when the dot hit the middle of the top pillar' );
This test.js file contains function assert and a few lines of test cases. You can ignore function assert for now, but these two calls to assert checks the following two collision cases:
Let's access the test page that we have just added. Go to URL like:
file:///XXXXX/mygames/flappydot/src/test/
(I.e., add /src/test/ after the URL for the game.) You should see something like this:
If you look at test.js, each assert line is a single test case. Currently our function is broken int the second test case. You can start fixing the function in src/coldetect.js to make sure that you get all "OK"'s.
NOTES: You don't have to make the collision detection perfect to make the game fun. For example, you don't have to think of the dot as a perfect shape that you have drawn or even a circle, but you can think of it as a simple rectangle which is large enough. Our dot is of size 40 x 40, but we have some curve. Therefore we can view it as a rectangle of size, say, 30 x 30 centered at the position of the player.
Clearly these two test cases are not enough to ensure that your checkPlayerPillarCollision works correctly. Therefore, after your code for checkPlayerPillarCollision passes these two test cases, you might want to add more test cases. The figure below shows a few other test cases that you should try adding to the test script.
Combining with the game mechanics
With method PillarPair.hit, we can regularly checks if the player hits the pillars. We create method update in GameLayer to do this.
update: function( dt ) {
if ( this.state == GameLayer.STATES.STARTED ) {
if ( this.pillarPair && this.pillarPair.hit( this.player ) ) {
// ... do something
}
}
},
Note that we only check collision when the game is started. To get this method update running, don't forget to call
this.scheduleUpdate();
in GameLayer.init.
What should we do when the player hits a pillar?
First, the game state should change. Let's add another state constant:
GameLayer.STATES = {
FRONT: 1,
STARTED: 2,
DEAD: 3
};
Also, we should stop the player and the pillars from moving. Let's write method endGame in GameLayer to do this:
endGame: function() {
this.player.stop();
if ( this.pillarPair ) {
this.pillarPair.unscheduleUpdate();
}
}
and add method Player.stop to class Player:
stop: function() {
this.started = false;
}
With this, the update method becomes:
update: function( dt ) {
if ( this.state == GameLayer.STATES.STARTED ) {
if ( this.pillarPair && this.pillarPair.hit( this.player ) ) {
this.endGame();
this.state = GameLayer.STATES.DEAD;
}
}
},
Try the game. You might want to try a few hit/miss at the pillars to manually test the collision detection function as well.
Exercise: Random the pillar heights
Let's create the pillar pairs with different heights. Our screen's height is 600 pixels, and we probably do not want the pillar to be too high or too low.
Exerciese: Add this method to PillarPair.
randomPositionY: function() {
// ... change the y position of the pillar pair.
},
Don't forget to call this method when (1) you create a pillar pair, and (2) when the piilar pair wraps around the screen.
Notes: When you test this method, you may want to switch off the collision detection. This can be easily done by adding return false; at the top of checkPlayerPillarCollision function. When you are done, don't forget to remove this hack.
Exercise: The series of pillar pairs
As we have most objects written nicely, adding more pillars can be done by creating more PillarPair's and spread them out nicely across the screen.
We only outline the changes that you need to make. It is your exercises to get all these steps done.
- PillarPair creation: Right now we only create one pillar pair. We should create more and keep them in an array. Make sure you spread them out nicely over the x co-ordinate. Don't forget to rename the method to createPillars (with the s).
- Collision detection: We can't just call this.pillarPair.hit( ... ). It would be nice to write a method for checking collision on all pillar pairs.
- endGame: Now we have to stop all the pillars.
Additional Exercises
There are many ways to make the game more fun like a real game. Here we list a few:
- After the player dies, the game stops. Implement the game restart feature, i.e., when the player hit any key after the player dies, the game should restart.
- The game shows no score. Add the scoring to the game.
- Flappy dot is extremely hard because you can hit only once. Add more lives to the player.
- In our game, the pillar pair can have different spacing. Implement this feature to make the game extremely hard.
- You can allow the dot to fire a few rockets to destroy the pillars.
Good luck. Have fun writing games!