ผลต่างระหว่างรุ่นของ "Prg2/arcade4 flappy dot"

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
(Prg2/arcade2 flappy dot ถูกเปลี่ยนชื่อเป็น Prg2/arcade3 flappy dot)
 
(ไม่แสดง 35 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน)
แถว 2: แถว 2:
  
 
In this tutorial, we will recreate a clone of a wonderful [http://en.wikipedia.org/wiki/Flappy_Bird 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.
 
In this tutorial, we will recreate a clone of a wonderful [http://en.wikipedia.org/wiki/Flappy_Bird 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 [[01219245/cocos2d-js/Sprites|Tutorial 100]] on basic sprites.
 
  
 
== Task breakdown ==
 
== Task breakdown ==
แถว 27: แถว 25:
  
 
== The player and its movement ==
 
== The player and its movement ==
 +
=== Create a new project and set up a Git repository ===
 +
We will start with an empty game template.  Put the following code in our main program <tt>flappy.py</tt>.
  
=== Create a new project, clean up, and set up a Git repository ===
+
{{synfile|flappy.py}}
You should start by downloading the template and cleaning up the original Hello World appFollow the instruction from the [[01219245/cocos2d-js/Sprites#Getting_started|previous tutorial]].
+
<syntaxhighlight lang="python">
 +
import arcade
 +
 +
SCREEN_WIDTH = 800
 +
SCREEN_HEIGHT = 600
 +
 +
class FlappyDotWindow(arcade.Window):
 +
    def __init__(self, width, height):
 +
        super().__init__(width, height)
 +
   
 +
        arcade.set_background_color(arcade.color.WHITE)
  
{{กล่องสี|#eeeeff|
+
    def on_draw(self):
'''If you use the full version of Cocos2d-JS or Cocos2d-x''', you should start by creating a new Cocos-JS project.
+
        arcade.start_render()
 
+
       
  cocos new -l js --no-native ชื่อโปรเจ็ค
+
   
 
+
def main():
This will create an example HelloWorld projectYou 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]].
+
    window = FlappyDotWindow(SCREEN_WIDTH, SCREEN_HEIGHT)
}}
+
    arcade.set_window(window)
 +
    arcade.run()
 +
   
 +
if __name__ == '__main__':
 +
    main()
 +
</syntaxhighlight>
  
Then, create a git repository at the project directory.
+
Try to run the game to see if an empty white window appears.
 +
Then, create a git repository at the project directory and commit the code.
  
 
{{gitcomment|Create your git repository.}}
 
{{gitcomment|Create your git repository.}}
  
=== Creating the sprite ===
+
=== Creating the dot model and the sprite ===
 
In this step, we shall create a sprite for the player, and show it in the middle of the screen.
 
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 <tt>res/images/dot.png</tt> and try to make it look cute.
+
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 <tt>images/dot.png</tt> and try to make it look cute.
  
We shall create class <tt>Player</tt> as <tt>src/Player.js</tt>.
+
We will continue our model/window code structure.  So let's create a dot model (called <tt>Player</tt>) and <tt>World</tt> in <tt>models.py</tt> as in our previous projects. Note that currently the Player do nothing in <tt>update</tt>
  
{{synfile|src/Player.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
var Player = cc.Sprite.extend({
+
class Player:
     ctor: function() {
+
     def __init__(self, world, x, y):
         this._super();
+
         self.world = world
         this.initWithFile( 'res/images/dot.png' );
+
         self.x = x
    }
+
        self.y = y
});
 
</syntaxhighlight>
 
  
We shall create the player in <tt>GameLayer.init</tt>.  To do so add these lines:
+
    def update(self, delta):
 +
        pass
  
{{synfile|src/GameLayer.js}}
+
class World:
<syntaxhighlight lang="javascript">
+
    def __init__(self, width, height):
         this.player = new Player();
+
        self.width = width
         this.player.setPosition( new cc.Point( screenWidth / 2, screenHeight / 2 ) );
+
         self.height = height
        this.addChild( this.player );
+
         this.player.scheduleUpdate();
+
         self.player = Player(self, width // 2, height // 2)
 +
 +
    def update(self, delta):
 +
         self.player.update(delta)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
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>.
+
As in the previous projects, we then use <tt>ModelSprite</tt> to display the sprite.  Add the class in <tt>flappy.py</tt> and update <tt>FlappyDotWindow</tt> accordingly.
  
{{synfile|main.js}}
+
{{synfile|flappy.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
 +
class ModelSprite(arcade.Sprite):
 +
    def __init__(self, *args, **kwargs):
 +
        self.model = kwargs.pop('model', None)
 +
 +
        super().__init__(*args, **kwargs)
 +
 +
    def sync_with_model(self):
 +
        if self.model:
 +
            self.set_position(self.model.x, self.model.y)
 +
 +
    def draw(self):
 +
        self.sync_with_model()
 +
        super().draw()
  
var screenWidth = 800;      // add these two constants
 
var screenHeight = 600;      //
 
  
cc.game.onStart = function(){
+
class FlappyDotWindow(arcade.Window):
    cc.view.adjustViewPort(true);
+
     def __init__(self, width, height):
     cc.view.setDesignResolutionSize(screenWidth, screenHeight, cc.ResolutionPolicy.SHOW_ALL);    // use them here
+
        super().__init__(width, height)
    // ...
 
};
 
cc.game.run();
 
</syntaxhighlight>
 
  
Last step is to update configuration and resource files.
+
        arcade.set_background_color(arcade.color.WHITE)
  
1. We need to add <tt>src/Player.js</tt> to the jsList in <tt>project.json</tt>.
+
        self.world = World(SCREEN_WIDTH, SCREEN_HEIGHT)
 +
       
 +
        self.dot_sprite = ModelSprite('images/dot.png', model=self.world.player)
  
{{synfile|project.json}}
+
     def update(self, delta):
<syntaxhighlight lang="javascript">
+
         self.world.update(delta)
     // ...
 
    "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.)
+
    def on_draw(self):
 +
        arcade.start_render()
  
{{synfile|src/resource.js}}
+
        self.dot_sprite.draw()
<syntaxhighlight lang="javascript">
+
</syntaxhighlight>  
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 run the game.  You should see your sprite in the middle of the screen.
  
 
{{gitcomment|Commit your work.}}
 
{{gitcomment|Commit your work.}}
แถว 120: แถว 135:
 
Let's look at the basics.  An object has a position, its position changes if it has non-zero velocity.
 
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>:
+
How can you change the player's position?  We can set its <tt>x</tt> and <tt>y</tt> attribute on the model.
 
 
this.setPosition( x, y );
 
  
 
If you want to apply the velocity, you can change the player position based on the velocity.
 
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.
+
If there is an acceleration, the object's velocity also changes.  The <tt>Player</tt> currently does not have <tt>velocity</tt> as its attribute, so we will add it.  Now, 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.
+
These attributes (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 ''arcade'', 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.)
+
While in Physics, everything is continuous, but 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>delta</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.
 
So the usual pseudo code for physics is as follows.
แถว 140: แถว 153:
 
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.   
 
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>:
+
Let's add this line that initialize property <tt>vy</tt> in <tt>Player</tt>'s initialization code:
 +
 
 +
{{synfile|models.py}}
 +
<syntaxhighlight lang="python">
 +
class Player:
 +
    def __init__(self, world, x, y):
 +
        # ... [old code hidden]
  
{{synfile|src/Player.js}}
+
         self.vy = 15
<syntaxhighlight lang="javascript">
 
         this.vy = 15;
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
แถว 151: แถว 168:
 
The <tt>update</tt> method changes the player's position
 
The <tt>update</tt> method changes the player's position
  
{{synfile|src/Player.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
    update: function( dt ) {
+
class Player:
        var pos = this.getPosition();
+
    # ...
         this.setPosition( new cc.Point( pos.x, pos.y + this.vy ) );
+
    def update(self, delta):
         this.vy += -1;
+
         self.y += self.vy
    }
+
         self.vy -= 1
 
</syntaxhighlight>
 
</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.
+
Note that we update <tt>self.vy</tt> at the end of update.  The constant <tt>-1</tt> is the acceleration.  The parameter <tt>delta</tt> represents delta time; we do not use it for now.
  
 
Try to run the program.  You should see the player falling.
 
Try to run the program.  You should see the player falling.
แถว 166: แถว 183:
 
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.
 
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>
+
Add these contants at the beginning of <tt>Player</tt> in <tt>models.py</tt>.  These are '''class variables'''.
  
{{synfile|src/Player.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
Player.G = -1;
+
class Player:
Player.STARTING_VELOCITY = 15;
+
    GRAVITY = 1
 +
    STARTING_VELOCITY = 15
 +
 
 +
    # ...
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Then replace <tt>15</tt> and <tt>-1</tt> in the code with the appropriate constants.
+
Then replace <tt>15</tt> and <tt>1</tt> in the code with the appropriate constants as follows.  Note that we can refer to these variables as <tt>self.GRAVITY</tt> or <tt>Player.GRAVITY</tt>.
 +
 
 +
{{synfile|models.py}}
 +
<syntaxhighlight lang="python">
 +
class Player:
 +
    # ...
 +
    def __init__(self, world, x, y):
 +
        # ...
 +
 
 +
        self.vy = Player.STARTING_VELOCITY
 +
 
 +
    def update(self, delta):
 +
        # ...
 +
        self.vy -= Player.GRAVITY
 +
</syntaxhighlight>
  
 
{{gitcomment|When your program looks good, commit it}}
 
{{gitcomment|When your program looks good, commit it}}
แถว 180: แถว 214:
 
=== Jumping dot ===
 
=== 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.
+
Now, let's make the dot jumps.  Let's add method <tt>jump</tt> that set the velocity to some positive amount.  Let's create a constant <tt>JUMPING_VELOCITY</tt> to represent this magic number as well.
  
{{synfile|src/Player.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
    jump: function() {
+
class Player:
        this.vy = Player.JUMPING_VELOCITY;
+
    # ...
     }
+
     JUMPING_VELOCITY = 15
</syntaxhighlight>
 
  
Also, add this constant after the class is defined in the <tt>.extend</tt> block.
+
    # ...
  
{{synfile|src/Player.js}}
+
    def jump(self):
<syntaxhighlight lang="javascript">
+
        self.vy = Player.JUMPING_VELOCITY
Player.JUMPING_VELOCITY = 15;
 
 
</syntaxhighlight>
 
</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.
 
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.
+
First, add method <tt>on_key_press</tt> in <tt>FlappyDotWindow</tt> to forward the call to the world.
  
{{synfile|src/GameLayer.js}}
+
{{synfile|flappy.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
    addKeyboardHandlers: function() {
+
class FlappyDotWindow(arcade.Window):
        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 ) {
+
     def on_key_press(self, key, key_modifiers):
    },
+
        self.world.on_key_press(key, key_modifiers)
 
 
    onKeyUp: function( keyCode, event ) {
 
    }
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Then, call addKeyboardHandlers in GameLayer.init
+
Then, in <tt>World</tt> for any key pressed, call <tt>jump</tt>.
  
{{synfile|src/GameLayer.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
    init: function() {
+
class World:
        // ...
+
    # ...
this.addKeyboardHandlers();
 
        // ...
 
    },
 
</syntaxhighlight>
 
  
We will jump in any key input, so we shall modify <tt>onKeyDown</tt> as follows.
+
     def on_key_press(self, key, key_modifiers):
 
+
         self.player.jump()
{{synfile|src/GameLayer.js}}
 
<syntaxhighlight lang="javascript">
 
     onKeyDown: function( keyCode, event ) {
 
         this.player.jump();
 
    }
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
แถว 245: แถว 255:
  
 
{{gitcomment|After a few trials to make sure your code works, please commit.}}
 
{{gitcomment|After a few trials to make sure your code works, please commit.}}
 +
 +
*********************************************************************************
 +
********************************* CHECK POINT 4.1 *******************************
 +
*********************************************************************************
  
 
== Game states ==
 
== 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.
+
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>World</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.
+
For now, the game has 2 states: <tt>FROZEN</tt> and <tt>STARTED</tt>.  After the game loads, its state is <tt>FROZEN</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).
+
Let's add the constants for states in class <tt>World</tt>:
  
{{synfile|src/GameLayer.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
GameLayer.STATES = {
+
class World:
     FRONT: 1,
+
     STATE_FROZEN = 1
     STARTED: 2
+
     STATE_STARTED = 2
};
+
    # ...
 
</syntaxhighlight>
 
</syntaxhighlight>
  
With this, we can refer to states as <tt>GameLayer.STATES.FRONT</tt> and <tt>GameLayer.STATES.STARTED</tt>.
+
With this, we can refer to states as <tt>World.STATE_FRONT</tt> and <tt>World.STATE_STARTED</tt>.
  
 
=== Initial state ===
 
=== 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>:
+
We let attribute <tt>state</tt> of <tt>World</tt> keeps the current game state.  We initialize the state in <tt>__init__</tt>:
  
{{synfile|src/GameLayer.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
this.state = GameLayer.STATES.FRONT;
+
class World:
 +
    # ...
 +
   
 +
    def __init__(self, width, height):
 +
        # ...
 +
 
 +
        self.state = World.STATE_FROZEN
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 
=== State transition ===
 
=== State transition ===
  
Our game changes its state when the user hits the keyboard, we rewrite method <tt>onKeyDown</tt> to
+
We add methods <tt>start</tt> and <tt>freeze</tt> to <tt>World</tt> so that <tt>FlappyDotWindow</tt> can tell the world to start working or to get frozenWe also include method <tt>is_started</tt> for querying the world's state.
 
 
{{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 dependentWhen 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|models.py}}
 +
<syntaxhighlight lang="python">
 +
class World:
 +
    # ...
 +
    def start(self):
 +
        self.state = World.STATE_STARTED
  
{{synfile|src/GameLayer.js}}
+
     def freeze(self):
<syntaxhighlight lang="javascript">
+
         self.state = World.STATE_FROZEN   
     onKeyDown: function( keyCode, event ) {
+
 
         if ( this.state == GameLayer.STATES.FRONT ) {
+
    def is_started(self):
            this.state = GameLayer.STATES.STARTED;
+
         return self.state == World.STATE_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>
 
</syntaxhighlight>
  
Or, you may want to use <tt>switch</tt> statement.
+
Our game changes its state when the user hits the keyboard.  We let <tt>FlappyDotWindow</tt> inform the world to get started.
  
=== Player's state ===
+
Rewrite method <tt>on_key_press</tt> in <tt>FlappyDotWindow</tt> to do that.
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}}
+
{{synfile|flappy.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
         this.started = false;
+
    def on_key_press(self, key, key_modifiers):
 +
         if not self.world.is_started():
 +
            self.world.start()
 +
           
 +
        self.world.on_key_press(key, key_modifiers)
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Add method <tt>start</tt> to update this state.
+
=== The world's state ===
  
{{synfile|src/Player.js}}
+
We have to change how the world behave according to its state.  Basically, it should not update anything when it is frozen.  Add the following <tt>if</tt> at the beginning of <tt>World.update</tt>
<syntaxhighlight lang="javascript">
 
    start: function() {
 
        this.started = true;
 
    }
 
</syntaxhighlight>
 
  
Finally, we only perform position update when the player is started.
+
<syntaxhighlight lang="python">
 +
class World:
 +
    # ...
  
{{synfile|src/Player.js}}
+
     def update(self, delta):
<syntaxhighlight lang="javascript">
+
         if self.state == World.STATE_FROZEN:
     update: function( dt ) {
+
             return
         if ( this.started ) {
+
       
             // ... old update code here
+
        # ...
        }
 
    }
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
We shall call
+
{{gitcomment|Try to see if the game works as expected. Commit your work after you are done.}}
 
 
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 expectedCommit your work after you are done.}}
+
*********************************************************************************
 +
********************************* CHECK POINT 4.2 *******************************
 +
  *********************************************************************************
  
 
== The pillar pair ==
 
== The pillar pair ==
แถว 367: แถว 367:
 
[[Image:Sprites-pillarpair2.png]]
 
[[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.
+
The blue object in the figure above contains two red sprite objects.  Specifically, we shall create a <tt>PillarPairSprite</tt> object that contains two sprites.
  
 
=== Sprite images ===
 
=== Sprite images ===
แถว 374: แถว 374:
 
[[Image:Clipped-pillars.png]]
 
[[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>.
+
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>images</tt>.
  
=== Node class ===
+
=== Model 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>.
+
Let's create class <tt>PillarPair</tt> in <tt>models.py</tt>.  Put the following code before class <tt>World</tt>.
  
{{synfile|src/PillarPair.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
var PillarPair = cc.Node.extend({
+
class PillarPair:
     ctor: function() {
+
     def __init__(self, world, x, y):
this._super();
+
        self.world = world
this.topPillar = cc.Sprite.create( 'res/images/pillar-top.png' );
+
        self.x = x
this.topPillar.setAnchorPoint( new cc.Point( 0.5, 0 ) );
+
        self.y = y
this.topPillar.setPosition( new cc.Point( 0, 100 ) );
+
 
this.addChild( this.topPillar );
+
    def update(self, delta):
+
        pass
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>
 
</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 PositionTo understand this, consider the top pillar.  We call
+
Currently we will have a single pillar pairSo create it in the world's initialization and make sure that we also call PillarPair's update in world's update:
 
 
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
+
{{synfile|models.py}}
 +
<syntaxhighlight lang="python">
 +
class World:
 +
    # ...
  
this.topPillar.setPosition( new cc.Point( 0, 100 ) );
+
    def __init__(self, width, height):
 +
        # ...
  
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).
+
        self.pillar_pair = PillarPair(self, width - 100, height // 2)
  
[[Image:Top-pillar-relative.png]]
+
    def update(self, delta):
 +
        # ...
  
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>.
+
         self.pillar_pair.update(delta)
 
 
{{synfile|src/GameLayer.js}}
 
<syntaxhighlight lang="javascript">
 
         this.pillarPair = new PillarPair();
 
        this.pillarPair.setPosition( new cc.Point( 700, 300 ) );
 
        this.addChild( this.pillarPair );
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
{{gitcomment|If the pillar pair appears, commit your change.}}
+
=== Pillar pair's sprite ===
  
<div class="toccolours mw-collapsible mw-collapsed">
+
We add the <tt>PillarPairSprite</tt> to draw <tt>PillarPair</tt>.  While we call it a sprite, for simplicity, it is actually not a subclass of <tt>arcade.Sprite</tt>.  This class basically deals with pillar pair drawing based on its <tt>model</tt> position (similar to ModelSprite).  Inside the pillar pair sprite object, we create two sprites for two pillars.
'''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 ===
+
{{synfile|flappy.py}}
Let's add the code that move the pillars, <tt>PillarPair.update</tt>.
+
<syntaxhighlight lang="python">
 
+
class PillarPairSprite():
{{synfile|src/PillarPair.js}}
+
     def __init__(self, model):
<syntaxhighlight lang="javascript">
+
        self.model = model
     update: function( dt ) {
+
       
         this.setPositionX( this.getPositionX() - 5 );
+
        self.top_pillar_sprite = arcade.Sprite('images/pillar-top.png')
    }
+
         self.bottom_pillar_sprite = arcade.Sprite('images/pillar-bottom.png')
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Then in <tt>GameLayer.init</tt>, add a line
+
To draw the pillar pair, we add the following method.
 +
 +
{{synfile|flappy.py}}
 +
<syntaxhighlight lang="python">
 +
class PillarPairSprite():
 +
    # ...
  
{{synfile|GameLayer.js}}
+
    def draw(self):
 
+
        self.top_pillar_sprite.set_position(self.model.x, self.model.y + 400)
this.pillarPair.scheduleUpdate();
+
        self.top_pillar_sprite.draw()
 
+
       
after the <tt>PillarPair</tt> is created.
+
        self.bottom_pillar_sprite.set_position(self.model.x, self.model.y - 400)
 
+
        self.bottom_pillar_sprite.draw()
{{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>
 
</syntaxhighlight>
  
Note that this file lists all images and probably other resources (e.g., sound files) that we want to use.
+
Please pay attention to how we draw the top pillar and the bottom pillar. Their positions are calculated in relative to the model's x and y.
  
Now, when you start our game, you'll see the Cocos2d-html5 preloading start screen.
+
Finally, let's add the sprite to the game and see if it appears.  Make sure you draw pillar pair sprite ''before'' drawing the player. (Why?)
  
{{gitcomment|If your code works, commit the changes.}}
+
{{synfile|flappy.py}}
 +
<syntaxhighlight lang="python">
 +
class FlappyDotWindow(arcade.Window):
 +
    def __init__(self, width, height):
 +
        # ...
 +
        self.pillar_pair_sprite = PillarPairSprite(model=self.world.pillar_pair)
  
=== 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>:
+
    def on_draw(self):
 +
        # ...
  
{{synfile|src/GameLayer.js}}
+
        self.pillar_pair_sprite.draw()
<syntaxhighlight lang="javascript">
+
         self.dot_sprite.draw()              # ... this is old code
    createPillarPair: function() {
 
        this.pillarPair = new PillarPair();
 
         this.pillarPair.setPosition( new cc.Point( 900, 300 ) );
 
        this.addChild( this.pillarPair );
 
        this.pillarPair.scheduleUpdate();
 
    }
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
In <tt>GameLayer.init</tt>, let's put this null initialization in place of the old pillar pair creation.
+
{{gitcomment|If the pillar pair appears, commit your change.}}
  
{{synfile|src/GameLayer.js}}
+
<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>
  
  this.pillarPair = null;
+
=== Moving the pillars ===
 +
Let's add the code that move the pillars, <tt>PillarPair.update</tt>. Note that we use another constant <tt>PILLAR_SPEED</tt> to make our code readable.
  
Finally, let's call <tt>createPillarPair</tt> after the game state changes to <tt>GameLayer.STATES.STARTED</tt> in <tt>onKeyDown</tt>:
+
{{synfile|models.py}}
 +
<syntaxhighlight lang="python">
  
{{synfile|src/GameLayer.js}}
+
class PillarPair:
<syntaxhighlight lang="javascript">
+
    PILLAR_SPEED = 5
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>.
+
     def update(self, delta):
 
+
         self.x -= PillarPair.PILLAR_SPEED
{{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>
 
</syntaxhighlight>
  
Test the code, and commit.
+
Try to see if it moves.
  
{{gitcomment|Commit your change.}}
+
{{gitcomment|Don't forget to 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 pillarsTo 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>.
+
  ********************************* CHECK POINT 4.3 *******************************
 
+
  *********************************************************************************
{{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 ===
 
=== Exercise: Reusing the pillars ===
แถว 559: แถว 496:
  
 
{{gitcomment|Test and commit the changes.}}
 
{{gitcomment|Test and commit the changes.}}
 +
 +
*********************************************************************************
 +
********************************* CHECK POINT 4.4 *******************************
 +
*********************************************************************************
  
 
== Exercise: Collision detection ==
 
== 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>:
+
We will check if our dot hits the pillar pair.  Our goal is to write method <tt>hit</tt> in class <tt>PillarPair</tt>.  '''But WAIT!! Don't start write it for now.  There will be more steps before we will do that.'''
  
{{synfile|src/PillarPair.js}}
+
<syntaxhighlight lang="python">
<syntaxhighlight lang="javascript">
+
     def hit(self, player):
     hit: function( player ) {
+
         # ....
         // return true if the player hits the pillar pair.
 
    }
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
แถว 581: แถว 520:
 
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.
 
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.
+
For simple functions, this is not very hard to do.  However, in this case, to call method <tt>hit</tt>, we have to create the world, the pillar pair and the player.  Note that since we have already decouple our model's code from <tt>arcade</tt> we can actually do that in our unit testing code.
  
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.
+
But since this is probably the first time you learn about unit testing, we will try to make the set-up very minimal by extracting the decision function as a stand-alone function called <tt>check_player_pillar_collision</tt> and test it independently of our other codes.  Let's finish method <tt>hit</tt> first.
  
{{synfile|src/PillarPair.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
    hit: function( player ) {
+
class PillarPair:
var playerPos = player.getPosition();
+
    # ...
var myPos = this.getPosition();
 
  
         return checkPlayerPillarCollision( playerPos.x, playerPos.y, myPos.x, myPos.y );
+
    def hit(self, player):
    }
+
         return check_player_pillar_collision(player.x, player.y,
 +
                                            self.x, self.y)
 
</syntaxhighlight>
 
</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.
+
We will write function <tt>check_player_pillar_collision</tt> in a separated module called <tt>coldetect</tt>, so that we can test it more independently.  Before we forget, let's add a code to import this function at the beginning of <tt>models.py</tt>.
 +
 
 +
{{synfile|models.py}}
 +
<syntaxhighlight lang="python">
 +
from coldetect import check_player_pillar_collision
 +
</syntaxhighlight>
  
 
Since this part is quite a detour, I'll give more detailed instructions.
 
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>.
+
Let's create a (broken) function <tt>check_player_pillar_collision</tt> in file <tt>coldetect.py</tt>.
  
{{synfile|src/coldetect.js}}
+
{{synfile|coldetect.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
var checkPlayerPillarCollision = function( playerX, playerY, pillarX, pillarY ) {
+
def check_player_pillar_collision(player_x, player_y, pillar_x, pillar_y):
     return false;
+
     return False
};
 
 
</syntaxhighlight>
 
</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 use Python's [https://docs.python.org/3.7/library/doctest.html doctest] to implement our unit testEach test and its expected output will be put as a document on the function.
  
We will test this function in a separated main JavaScript programWe will put all these testing codes in a separated directory <tt>src/test</tt>.
+
So let's start adding a few test casesChange the function <tt>check_player_pillar_collision</tt> to be
  
Let's create directory <tt>src/test</tt>.
+
{{synfile|coldetect.py}}
 +
<syntaxhighlight lang="python">
 +
def check_player_pillar_collision(player_x, player_y, pillar_x, pillar_y):
 +
    """
 +
    When the dot is very far left of the pillar pair
  
Add <tt>index.html</tt> in <tt>src/test</tt>:
+
    >>> check_player_pillar_collision(100, 100, 300, 200)
 +
    False
  
{{synfile|src/test/index.html}}
+
    When the dot hit the middle of the top pillar
<syntaxhighlight lang="html5">
+
 
<!doctype html>
+
    >>> check_player_pillar_collision( 300, 300, 300, 200 )
<html lang="en">
+
     True
  <body>
+
     """
     <ul id="test-output">
+
 
     </ul>
+
    return False
    <script src="../coldetect.js"></script>
 
    <script src="test.js"></script>
 
  </body>
 
</html>
 
 
</syntaxhighlight>
 
</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>:
+
We put each test case as a function call in the docstring of the function.  Each line that started with ">>>" (that looks like you are typing in Python's console) is a test case. Its expected output is on the next lineIn the above code, we have two test cases.
  
{{synfile|src/test/test.js}}
+
We can call the doctest framework to test our code by calling:
<syntaxhighlight lang="javascript">
 
var assert = function( actual, expected, message ) {
 
    var msg = "";
 
  
    if ( actual === expected ) {
+
python -m doctest coldetect.py -v
msg = '<span style="color: green">OK</span>';
+
 
    } else {
+
You will get the following output:
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,
+
<pre>
'when the dot is very far left of the pillar pair' );
+
Trying:
assert( checkPlayerPillarCollision( 300, 300, 300, 200 ), true,
+
    check_player_pillar_collision(100, 100, 300, 200)
'when the dot hit the middle of the top pillar' );
+
Expecting:
</syntaxhighlight>
+
    False
 +
ok
 +
Trying:
 +
    check_player_pillar_collision( 300, 300, 300, 200 )
 +
Expecting:
 +
    True
 +
**********************************************************************
 +
File "/xxxxxxxx/flappy/coldetect.py", line 10, in coldetect.check_player_pillar_collision
 +
Failed example:
 +
    check_player_pillar_collision( 300, 300, 300, 200 )
 +
Expected:
 +
    True
 +
Got:
 +
    False
 +
1 items had no tests:
 +
    coldetect
 +
**********************************************************************
 +
1 items had failures:
 +
  1 of   2 in coldetect.check_player_pillar_collision
 +
2 tests in 2 items.
 +
1 passed and 1 failed.
 +
***Test Failed*** 1 failures.
 +
</pre>
  
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:
+
It says that our function is broken in the second test case.  You can start fixing the function in <tt>coldetect.py</tt> to make sure that you get both test passed.  That would look like the following.
  
[[Image:Test-coldetect-testcases.png]]
+
<pre>
 +
Trying:
 +
    check_player_pillar_collision(100, 100, 300, 200)
 +
Expecting:
 +
    False
 +
ok
 +
Trying:
 +
    check_player_pillar_collision( 300, 300, 300, 200 )
 +
Expecting:
 +
    True
 +
ok
 +
1 items had no tests:
 +
    coldetect
 +
1 items passed all tests:
 +
  2 tests in coldetect.check_player_pillar_collision
 +
2 tests in 2 items.
 +
2 passed and 0 failed.
 +
Test passed.
 +
</pre>
  
Let's access the test page that we have just addedGo to URL like:
+
'''NOTES 1:'''  You can remove the option <tt>-v</tt> when calling doctest to let it show only broken test casesFor example, if we run it with
  
  file:///XXXXX/mygames/flappydot/src/test/
+
  python -m doctest coldetect.py
  
(I.e., add <tt>/src/test/</tt> after the URL for the game.)  You should see something like this:
+
You will see
  
[[Image:Test-coldetect.png]]
+
<pre>
 +
**********************************************************************
 +
File "/home/jittat/prog/arcade/flappy/coldetect.py", line 10, in coldetect.check_player_pillar_collision
 +
Failed example:
 +
    check_player_pillar_collision( 300, 300, 300, 200 )
 +
Expected:
 +
    True
 +
Got:
 +
    False
 +
**********************************************************************
 +
1 items had failures:
 +
  1 of  2 in coldetect.check_player_pillar_collision
 +
***Test Failed*** 1 failures.
 +
</pre>
  
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.
+
You will get an empty response if everything works.
  
'''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.
+
'''NOTES 2:''' 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.
+
Clearly these two test cases are not enough to ensure that your <tt>check_player_pillar_collision</tt> works correctly.  Therefore, after your code for <tt>check_player_pillar_collision</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]]
 
[[Image:Test-coldetect-moretests.png]]
แถว 676: แถว 664:
 
=== Combining with the game mechanics ===
 
=== 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.
+
With method <tt>PillarPair.hit</tt>, we can regularly checks if the player hits the pillars inside our world's code.
 
 
{{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
+
{{synfile|models.py}}
 +
<syntaxhighlight lang="python">
 +
class World:
 +
    # ...
  
this.scheduleUpdate();
+
    def update(self, delta):
 +
        # ...
  
in <tt>GameLayer.init</tt>.
+
        if self.pillar_pair.hit(self.player):
 +
            # ... do something here
 +
</syntaxhighlight>
  
 
What should we do when the player hits a pillar?
 
What should we do when the player hits a pillar?
  
First, the game state should change.  Let's add another state constant:  
+
First, the game state should change.  Let's add another state constant and its related methods:  
  
{{synfile|src/GameLayer.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
GameLayer.STATES = {
+
class World:
     FRONT: 1,
+
     STATE_FROZEN = 1     # .. old code
     STARTED: 2,
+
     STATE_STARTED = 2     # .. old code
     DEAD: 3
+
     STATE_DEAD = 3
};
+
   
</syntaxhighlight>
+
    # ...
 +
   
 +
    def die(self):
 +
        self.state = World.STATE_DEAD
  
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:
+
     def is_dead(self):
 
+
         return self.state == World.STATE_DEAD
{{synfile|src/GameLayer.js}}
 
<syntaxhighlight lang="javascript">
 
     endGame: function() {
 
         this.player.stop();
 
        if ( this.pillarPair ) {
 
            this.pillarPair.unscheduleUpdate();
 
        }
 
    }
 
 
</syntaxhighlight>
 
</syntaxhighlight>
  
and add method <tt>Player.stop</tt> to class <tt>Player</tt>:
+
With that state, we can change our update function to
  
{{synfile|src/Player.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
    stop: function() {
+
class World:
this.started = false;
+
    # ...
    }
 
</syntaxhighlight>
 
  
With this, the <tt>update</tt> method becomes:
+
    def update(self, delta):
 +
        if self.state in [World.STATE_FROZEN, World.STATE_DEAD]:
 +
            return
 +
       
 +
        self.player.update(delta)
 +
        self.pillar_pair.update(delta)
  
{{synfile|src/GameLayer.js}}
+
         if self.pillar_pair.hit(self.player):
<syntaxhighlight lang="javascript">
+
            self.die()
    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>
 
</syntaxhighlight>
  
แถว 746: แถว 719:
  
 
{{gitcomment|The game should be somewhat playable.  Don't forget to commit your work.}}
 
{{gitcomment|The game should be somewhat playable.  Don't forget to commit your work.}}
 +
 +
*********************************************************************************
 +
********************************* CHECK POINT 4.5 *******************************
 +
*********************************************************************************
  
 
== Exercise: Random the pillar heights ==
 
== Exercise: Random the pillar heights ==
แถว 752: แถว 729:
 
'''Exerciese:''' Add this method to <tt>PillarPair</tt>.
 
'''Exerciese:''' Add this method to <tt>PillarPair</tt>.
  
{{synfile|src/PillarPair.js}}
+
{{synfile|models.py}}
<syntaxhighlight lang="javascript">
+
<syntaxhighlight lang="python">
     randomPositionY: function() {
+
class PillarPair:
         // ... change the y position of the pillar pair.
+
     # ...
    },
+
 
 +
    def random_position_y(self):
 +
         # ... change the y position of the pillar pair.
 
</syntaxhighlight>
 
</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.
 
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.
+
'''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>check_player_pillar_collision</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>.}}
+
{{gitcomment|Commit your work.  Don't forget to remove the hack to avoid <tt>check_player_pillar_collision</tt>.}}
  
 
== Exercise: The series of pillar pairs ==
 
== 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.
 
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.
 
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''').
+
* '''PillarPair creation:'''  Right now we only create one pillar pair.  We should create more and keep them in a list. Make sure you spread them out nicely over the x co-ordinate.  It would be good if you extract the pillar creation function from the world's initialization code.
* '''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.
+
* '''Collision detection:''' We can't just call <tt>self.pillar_pair.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.}}
 
{{gitcomment|For each of these step, don't forget to commit when you get a unit of work done.}}
 +
 +
*********************************************************************************
 +
********************************* CHECK POINT 4.6 *******************************
 +
*********************************************************************************
  
 
== Additional Exercises ==
 
== Additional Exercises ==

รุ่นแก้ไขปัจจุบันเมื่อ 06:06, 14 กุมภาพันธ์ 2562

This is part of the course Programming 2, the material is originally from 01219245/cocos2d-js/Sprites2 from 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.

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:

219245-dotscr.png

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 and set up a Git repository

We will start with an empty game template. Put the following code in our main program flappy.py.

File: flappy.py
import arcade
 
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
 
class FlappyDotWindow(arcade.Window):
    def __init__(self, width, height):
        super().__init__(width, height)
 
        arcade.set_background_color(arcade.color.WHITE)

    def on_draw(self):
        arcade.start_render()
        
 
def main():
    window = FlappyDotWindow(SCREEN_WIDTH, SCREEN_HEIGHT)
    arcade.set_window(window)
    arcade.run()
 
if __name__ == '__main__':
    main()

Try to run the game to see if an empty white window appears. Then, create a git repository at the project directory and commit the code.

Gitmark.png Create your git repository.

Creating the dot model and 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 images/dot.png and try to make it look cute.

We will continue our model/window code structure. So let's create a dot model (called Player) and World in models.py as in our previous projects. Note that currently the Player do nothing in update

File: models.py
class Player:
    def __init__(self, world, x, y):
        self.world = world
        self.x = x
        self.y = y

    def update(self, delta):
        pass

class World:
    def __init__(self, width, height):
        self.width = width
        self.height = height
 
        self.player = Player(self, width // 2, height // 2)
 
     def update(self, delta):
        self.player.update(delta)

As in the previous projects, we then use ModelSprite to display the sprite. Add the class in flappy.py and update FlappyDotWindow accordingly.

File: flappy.py
class ModelSprite(arcade.Sprite):
    def __init__(self, *args, **kwargs):
        self.model = kwargs.pop('model', None)
 
        super().__init__(*args, **kwargs)
 
    def sync_with_model(self):
        if self.model:
            self.set_position(self.model.x, self.model.y)
 
    def draw(self):
        self.sync_with_model()
        super().draw()


class FlappyDotWindow(arcade.Window):
    def __init__(self, width, height):
        super().__init__(width, height)

        arcade.set_background_color(arcade.color.WHITE)

        self.world = World(SCREEN_WIDTH, SCREEN_HEIGHT)
        
        self.dot_sprite = ModelSprite('images/dot.png', model=self.world.player)

    def update(self, delta):
        self.world.update(delta)

    def on_draw(self):
        arcade.start_render()

        self.dot_sprite.draw()

Try to run the game. You should see your sprite in the middle of the screen.

Gitmark.png 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? We can set its x and y attribute on the model.

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 Player currently does not have velocity as its attribute, so we will add it. Now, you can update the velocity based on the acceleration.

These attributes (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 arcade, i.e., for the y-axis, we think of the direction as going upwards.

While in Physics, everything is continuous, but 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 delta, 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's initialization code:

File: models.py
class Player:
    def __init__(self, world, x, y):
        # ... [old code hidden]

        self.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

File: models.py
class Player:
    # ...
    def update(self, delta):
        self.y += self.vy
        self.vy -= 1

Note that we update self.vy at the end of update. The constant -1 is the acceleration. The parameter delta 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 contants at the beginning of Player in models.py. These are class variables.

File: models.py
class Player:
    GRAVITY = 1
    STARTING_VELOCITY = 15

    # ...

Then replace 15 and 1 in the code with the appropriate constants as follows. Note that we can refer to these variables as self.GRAVITY or Player.GRAVITY.

File: models.py
class Player:
    # ...
    def __init__(self, world, x, y):
        # ... 

        self.vy = Player.STARTING_VELOCITY

    def update(self, delta):
        # ...
        self.vy -= Player.GRAVITY
Gitmark.png When your program looks good, commit it

Jumping dot

Now, let's make the dot jumps. Let's add method jump that set the velocity to some positive amount. Let's create a constant JUMPING_VELOCITY to represent this magic number as well.

File: models.py
class Player:
    # ...
    JUMPING_VELOCITY = 15

    # ...

    def jump(self):
        self.vy = Player.JUMPING_VELOCITY

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 method on_key_press in FlappyDotWindow to forward the call to the world.

File: flappy.py
class FlappyDotWindow(arcade.Window):
    # ...

    def on_key_press(self, key, key_modifiers):
         self.world.on_key_press(key, key_modifiers)

Then, in World for any key pressed, call jump.

File: models.py
class World:
    # ...

    def on_key_press(self, key, key_modifiers):
        self.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.

Gitmark.png After a few trials to make sure your code works, please commit.
*********************************************************************************
********************************* CHECK POINT 4.1 *******************************
*********************************************************************************

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 World, 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: FROZEN and STARTED. After the game loads, its state is FROZEN. 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 in class World:

File: models.py
class World:
    STATE_FROZEN = 1
    STATE_STARTED = 2
    # ...

With this, we can refer to states as World.STATE_FRONT and World.STATE_STARTED.

Initial state

We let attribute state of World keeps the current game state. We initialize the state in __init__:

File: models.py
class World:
    # ...
    
    def __init__(self, width, height):
        # ...

        self.state = World.STATE_FROZEN

State transition

We add methods start and freeze to World so that FlappyDotWindow can tell the world to start working or to get frozen. We also include method is_started for querying the world's state.

File: models.py
class World:
    # ...
    def start(self):
        self.state = World.STATE_STARTED

    def freeze(self):
        self.state = World.STATE_FROZEN     
   
    def is_started(self):
        return self.state == World.STATE_STARTED

Our game changes its state when the user hits the keyboard. We let FlappyDotWindow inform the world to get started.

Rewrite method on_key_press in FlappyDotWindow to do that.

File: flappy.py
    def on_key_press(self, key, key_modifiers):
        if not self.world.is_started():
            self.world.start()
            
        self.world.on_key_press(key, key_modifiers)

The world's state

We have to change how the world behave according to its state. Basically, it should not update anything when it is frozen. Add the following if at the beginning of World.update

class World:
    # ...

    def update(self, delta):
        if self.state == World.STATE_FROZEN:
            return
        
        # ...
Gitmark.png Try to see if the game works as expected. Commit your work after you are done.
*********************************************************************************
********************************* CHECK POINT 4.2 *******************************
*********************************************************************************

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.

Sprites-pillarpair1.png

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.

Sprites-pillarpair2.png

The blue object in the figure above contains two red sprite objects. Specifically, we shall create a PillarPairSprite 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.

Clipped-pillars.png

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 images.

Model class

Let's create class PillarPair in models.py. Put the following code before class World.

File: models.py
class PillarPair:
    def __init__(self, world, x, y):
        self.world = world
        self.x = x
        self.y = y

    def update(self, delta):
        pass

Currently we will have a single pillar pair. So create it in the world's initialization and make sure that we also call PillarPair's update in world's update:

File: models.py
class World:
    # ...

    def __init__(self, width, height):
        # ...

        self.pillar_pair = PillarPair(self, width - 100, height // 2)

    def update(self, delta):
        # ...

        self.pillar_pair.update(delta)

Pillar pair's sprite

We add the PillarPairSprite to draw PillarPair. While we call it a sprite, for simplicity, it is actually not a subclass of arcade.Sprite. This class basically deals with pillar pair drawing based on its model position (similar to ModelSprite). Inside the pillar pair sprite object, we create two sprites for two pillars.

File: flappy.py
class PillarPairSprite():
    def __init__(self, model):
        self.model = model
        
        self.top_pillar_sprite = arcade.Sprite('images/pillar-top.png')
        self.bottom_pillar_sprite = arcade.Sprite('images/pillar-bottom.png')

To draw the pillar pair, we add the following method.

File: flappy.py
class PillarPairSprite():
    # ...

    def draw(self):
        self.top_pillar_sprite.set_position(self.model.x, self.model.y + 400)
        self.top_pillar_sprite.draw()
        
        self.bottom_pillar_sprite.set_position(self.model.x, self.model.y - 400)
        self.bottom_pillar_sprite.draw()

Please pay attention to how we draw the top pillar and the bottom pillar. Their positions are calculated in relative to the model's x and y.

Finally, let's add the sprite to the game and see if it appears. Make sure you draw pillar pair sprite before drawing the player. (Why?)

File: flappy.py
class FlappyDotWindow(arcade.Window):
    def __init__(self, width, height):
        # ...
        self.pillar_pair_sprite = PillarPairSprite(model=self.world.pillar_pair)

    # ...

    def on_draw(self):
        # ...

        self.pillar_pair_sprite.draw()
        self.dot_sprite.draw()              # ... this is old code
Gitmark.png If the pillar pair appears, commit your change.

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. Note that we use another constant PILLAR_SPEED to make our code readable.

File: models.py
class PillarPair:
    PILLAR_SPEED = 5
    
    # ...

    def update(self, delta):
        self.x -= PillarPair.PILLAR_SPEED

Try to see if it moves.

Gitmark.png Don't forget to commit.
*********************************************************************************
********************************* CHECK POINT 4.3 *******************************
*********************************************************************************

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.

Gitmark.png Test and commit the changes.
*********************************************************************************
********************************* CHECK POINT 4.4 *******************************
*********************************************************************************

Exercise: Collision detection

We will check if our dot hits the pillar pair. Our goal is to write method hit in class PillarPair. But WAIT!! Don't start write it for now. There will be more steps before we will do that.

    def hit(self, player):
        # ....

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 simple functions, this is not very hard to do. However, in this case, to call method hit, we have to create the world, the pillar pair and the player. Note that since we have already decouple our model's code from arcade we can actually do that in our unit testing code.

But since this is probably the first time you learn about unit testing, we will try to make the set-up very minimal by extracting the decision function as a stand-alone function called check_player_pillar_collision and test it independently of our other codes. Let's finish method hit first.

File: models.py
class PillarPair:
    # ...

    def hit(self, player):
        return check_player_pillar_collision(player.x, player.y,
                                             self.x, self.y)

We will write function check_player_pillar_collision in a separated module called coldetect, so that we can test it more independently. Before we forget, let's add a code to import this function at the beginning of models.py.

File: models.py
from coldetect import check_player_pillar_collision

Since this part is quite a detour, I'll give more detailed instructions.

Let's create a (broken) function check_player_pillar_collision in file coldetect.py.

File: coldetect.py
def check_player_pillar_collision(player_x, player_y, pillar_x, pillar_y):
    return False

We will use Python's doctest to implement our unit test. Each test and its expected output will be put as a document on the function.

So let's start adding a few test cases. Change the function check_player_pillar_collision to be

File: coldetect.py
def check_player_pillar_collision(player_x, player_y, pillar_x, pillar_y):
    """
    When the dot is very far left of the pillar pair

    >>> check_player_pillar_collision(100, 100, 300, 200)
    False

    When the dot hit the middle of the top pillar

    >>> check_player_pillar_collision( 300, 300, 300, 200 )
    True
    """

    return False

We put each test case as a function call in the docstring of the function. Each line that started with ">>>" (that looks like you are typing in Python's console) is a test case. Its expected output is on the next line. In the above code, we have two test cases.

We can call the doctest framework to test our code by calling:

python -m doctest coldetect.py -v

You will get the following output:

Trying:
    check_player_pillar_collision(100, 100, 300, 200)
Expecting:
    False
ok
Trying:
    check_player_pillar_collision( 300, 300, 300, 200 )
Expecting:
    True
**********************************************************************
File "/xxxxxxxx/flappy/coldetect.py", line 10, in coldetect.check_player_pillar_collision
Failed example:
    check_player_pillar_collision( 300, 300, 300, 200 )
Expected:
    True
Got:
    False
1 items had no tests:
    coldetect
**********************************************************************
1 items had failures:
   1 of   2 in coldetect.check_player_pillar_collision
2 tests in 2 items.
1 passed and 1 failed.
***Test Failed*** 1 failures.

It says that our function is broken in the second test case. You can start fixing the function in coldetect.py to make sure that you get both test passed. That would look like the following.

Trying:
    check_player_pillar_collision(100, 100, 300, 200)
Expecting:
    False
ok
Trying:
    check_player_pillar_collision( 300, 300, 300, 200 )
Expecting:
    True
ok
1 items had no tests:
    coldetect
1 items passed all tests:
   2 tests in coldetect.check_player_pillar_collision
2 tests in 2 items.
2 passed and 0 failed.
Test passed.

NOTES 1: You can remove the option -v when calling doctest to let it show only broken test cases. For example, if we run it with

python -m doctest coldetect.py

You will see

**********************************************************************
File "/home/jittat/prog/arcade/flappy/coldetect.py", line 10, in coldetect.check_player_pillar_collision
Failed example:
    check_player_pillar_collision( 300, 300, 300, 200 )
Expected:
    True
Got:
    False
**********************************************************************
1 items had failures:
   1 of   2 in coldetect.check_player_pillar_collision
***Test Failed*** 1 failures.

You will get an empty response if everything works.

NOTES 2: 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 check_player_pillar_collision works correctly. Therefore, after your code for check_player_pillar_collision 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.

Test-coldetect-moretests.png

Gitmark.png Don't forget to commit your work at this point.

Combining with the game mechanics

With method PillarPair.hit, we can regularly checks if the player hits the pillars inside our world's code.

File: models.py
class World:
    # ...

    def update(self, delta):
        # ...

        if self.pillar_pair.hit(self.player):
            # ... do something here

What should we do when the player hits a pillar?

First, the game state should change. Let's add another state constant and its related methods:

File: models.py
class World:
    STATE_FROZEN = 1      # .. old code
    STATE_STARTED = 2     # .. old code
    STATE_DEAD = 3
    
    # ...
    
    def die(self):
        self.state = World.STATE_DEAD

    def is_dead(self):
        return self.state == World.STATE_DEAD

With that state, we can change our update function to

File: models.py
class World:
    # ...

    def update(self, delta):
        if self.state in [World.STATE_FROZEN, World.STATE_DEAD]:
            return
        
        self.player.update(delta)
        self.pillar_pair.update(delta)

        if self.pillar_pair.hit(self.player):
            self.die()

Try the game. You might want to try a few hit/miss at the pillars to manually test the collision detection function as well.

Gitmark.png The game should be somewhat playable. Don't forget to commit your work.
*********************************************************************************
********************************* CHECK POINT 4.5 *******************************
*********************************************************************************

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.

File: models.py
class PillarPair:
    # ...

    def random_position_y(self):
        # ... 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 check_player_pillar_collision function. When you are done, don't forget to remove this hack.

Gitmark.png Commit your work. Don't forget to remove the hack to avoid check_player_pillar_collision.

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 a list. Make sure you spread them out nicely over the x co-ordinate. It would be good if you extract the pillar creation function from the world's initialization code.
  • Collision detection: We can't just call self.pillar_pair.hit( ... ). It would be nice to write a method for checking collision on all pillar pairs.
Gitmark.png For each of these step, don't forget to commit when you get a unit of work done.
*********************************************************************************
********************************* CHECK POINT 4.6 *******************************
*********************************************************************************

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!

Gitmark.png Don't forget to regularly commit your work.