Prg2/arcade5 maze

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา
This is part of the course Programming 2, the material is originally from 01219245/cocos2d/Maze from 01219245.

Rough steps

We will follow these steps to implement a pacman-like game.

  • Shows and moves pacman
  • Shows maze
  • Moves pacman in the maze, while respecting walls.

The Pacman and its movements

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 maze.py.

File: maze.py
import arcade
 
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
 
class MazeWindow(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 = MazeWindow(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 and make the first commit.

Creating the player 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 Pacman:
    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.pacman = Pacman(self, width // 2, height // 2)
 
     def update(self, delta):
        self.pacman.update(delta)

We will import these classes into our main program.

File: maze.py
from models import World, Pacman

We will show the pacman. Download sprite images from https://theory.cpe.ku.ac.th/~jittat/courses/ooplab/pacman/. Save pacman.png in directory images.

As in the previous projects, we then use ModelSprite to display the sprite. Add the class in maze.py.

File: maze.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()

Then update MazeWindow to include the world and create ModelSprite accordingly.

File: maze.py
class MazeWindow(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.pacman_sprite = ModelSprite('images/pacman.png',
                                         model=self.world.pacman)

    def update(self, delta):
        self.world.update(delta)
        
    def on_draw(self):
        arcade.start_render()

        self.pacman_sprite.draw()

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

Gitmark.png Commit your work.

Movement

Models

We start by adding direction constants as in the snake game at the top of models.py

File: models.py
DIR_STILL = 0
DIR_UP = 1
DIR_RIGHT = 2
DIR_DOWN = 3
DIR_LEFT = 4
 
DIR_OFFSETS = { DIR_STILL: (0,0),
                DIR_UP: (0,1),
                DIR_RIGHT: (1,0),
                DIR_DOWN: (0,-1),
                DIR_LEFT: (-1,0) }

We add attribute direction to Pacman (initialized to DIR_STILL). We add method move to deal with Pacman's movements based on its direction. We call move in update.

File: models.py
class Pacman:
    def __init__(self, world, x, y):
        # ...
        self.direction = DIR_STILL

    def move(self, direction):
        self.x += DIR_OFFSETS[direction][0]
        self.y += DIR_OFFSETS[direction][1]

    def update(self, delta):
        self.move(self.direction)

Events

In this game, we need to deal with key pressed and key released (because we want pacman to stop if we release the keys).

To deal with key events, we first wire the on_key_press and on_key_release from the window to the world.

File: maze.py
class MazeWindow(arcade.Window):
    # ...

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

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

In class World, you have to figure out how to deal with it. First, you need to import the key code constants:

File: models.py
import arcade.key

Then, in on_key_press, we can update the pacman's direction based on the key pressed. When a key is released, we let Pacman stops.

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

    def on_key_press(self, key, key_modifiers):
        if key == arcade.key.UP:
            self.pacman.direction = DIR_UP
        if key == arcade.key.DOWN:
            self.pacman.direction = DIR_DOWN
        if key == arcade.key.LEFT:
            self.pacman.direction = DIR_LEFT
        if key == arcade.key.RIGHT:
            self.pacman.direction = DIR_RIGHT

    def on_key_release(self, key, key_modifiers):
        self.pacman.direction = DIR_STILL

Try to run the code. Try play with key release handling by trying to press new arrow key after the previous one without releasing the key, then try to release the first key and see how the program behaves. Explain why.

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

Faster

The pacman moves fairly slowly. Let's add a default speed, by adding another constants in models.py.

File: models.py
MOVEMENT_SPEED = 5

And update Pacman.move to use this constant.

File: models.py
class Pacman:
    # ...

    def move(self, direction):
        self.x += MOVEMENT_SPEED * DIR_OFFSETS[direction][0]
        self.y += MOVEMENT_SPEED * DIR_OFFSETS[direction][1]
Gitmark.png Don't forget to commit.
*********************************************************************************
********************************* CHECK POINT 5.1 *******************************
*********************************************************************************

The maze

Our maze consists of a set of smaller 40x40 sprites. Let's create an image for the wall try to show that on the screen.

Create a 40x40 block image and save it in images/wall.png. (You can also download it from https://theory.cpe.ku.ac.th/~jittat/courses/ooplab/pacman/.

Let's create a Maze class that keeps the maze information. The key question here is how to keep the maze information. In a simple game with one maze like this, we can simply store the maze as a constant in our code. However, if you have many maze levels, you might want to load the data from files. (We shall discuss that later, hopefully.)

The code below shows Maze class in src/Maze.js. Note that since our screen is of size 800 x 600 and our wall image is of size 40 x 40, we can have 20 x 15 units. In our case, we leave the top row and the bottom row out, so the height of the maze is only 13 units.

File: maze.py
class Maze:
    def __init__(self, world):
        self.map = [ '####################',
                     '#..................#',
                     '#.###.###..###.###.#',
                     '#.#...#......#...#.#',
                     '#.#.###.####.###.#.#',
                     '#.#.#..........#.#.#',
                     '#.....###. ###.....#',
                     '#.#.#..........#.#.#',
                     '#.#.###.####.###.#.#',
                     '#.#...#......#...#.#',
                     '#.###.###..###.###.#',
                     '#..................#',
                     '####################' ]
        self.height = len(self.map)
        self.width = len(self.map[0])

We also put additional has_wall_at and has_dot_at methods for accessing maze information.

File: maze.py
class Maze:
    # ...

    def has_wall_at(self, r, c):
        return self.map[r][c] == '#'

    def has_dot_at(self, r, c):
        return self.map[r][c] == '.'

Let's create a maze object when we initialize the world.

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

        self.maze = Maze(self)

MazeDrawer

To draw the maze, we create class MazeDrawer in maze.py

File: maze.py
	for ( var r = 0; r < this.HEIGHT; r++ ) {
	    for ( var c = 0; c < this.WIDTH; c++ ) {
		if ( this.MAP[ r ][ c ] == '#' ) {
		    var s = cc.Sprite.create( 'res/images/wall.png' );
		    s.setAnchorPoint( cc.p( 0, 0 ) );
		    s.setPosition( cc.p( c * 40, (this.HEIGHT - r - 1) * 40 ) );
		    this.addChild( s );
		}
	    }
	}

Note that we use function cc.p(...) as a short cut for new cc.Point(...). Also, note how we put anchor points at the lower-left corners of all wall sprites and place all sprites in appropriate locations.

NOTES (IMPORTANT): We also use (this.HEIGHT - r - 1) to calculate the y-co-ordinate of the sprite. This is because the y-co-ordinates run in an opposite direction of the map data. (If we do not do this, our maze will flip vertically. You can try that)

We also put the anchor point for the maze at its lower-left corner as well. (Put this code also in method Maze.ctor.)

	this.setAnchorPoint( cc.p( 0, 0 ) );

To use this Maze in GameLayer we simply create it in GameLayer.init:

	this.maze = new Maze();
	this.maze.setPosition( cc.p( 0, 40 ) );
        this.addChild( this.maze );

We are ready to see our map. Let's load the game.

Gitmark.png Don't forget to commit this nice scene.

Moving inside the maze