ผลต่างระหว่างรุ่นของ "Prg2/arcade5 maze"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
(ไม่แสดง 57 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
แถว 166: | แถว 166: | ||
==== Events ==== | ==== Events ==== | ||
− | In this game, we need to deal with key pressed | + | In this game, we need to deal with key pressed We will not handle key releases because we want Pacman to keep moving even after we release the key. |
− | To deal with key events, we first wire the <tt>on_key_press | + | To deal with key events, we first wire the <tt>on_key_press</tt> from the window to the world. |
{{synfile|maze.py}} | {{synfile|maze.py}} | ||
แถว 177: | แถว 177: | ||
def on_key_press(self, key, key_modifiers): | def on_key_press(self, key, key_modifiers): | ||
self.world.on_key_press(key, key_modifiers) | self.world.on_key_press(key, key_modifiers) | ||
− | |||
− | |||
− | |||
</syntaxhighlight> | </syntaxhighlight> | ||
แถว 205: | แถว 202: | ||
if key == arcade.key.RIGHT: | if key == arcade.key.RIGHT: | ||
self.pacman.direction = DIR_RIGHT | self.pacman.direction = DIR_RIGHT | ||
− | |||
− | |||
− | |||
</syntaxhighlight> | </syntaxhighlight> | ||
แถว 220: | แถว 214: | ||
{{synfile|models.py}} | {{synfile|models.py}} | ||
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
− | MOVEMENT_SPEED = | + | MOVEMENT_SPEED = 4 |
</syntaxhighlight> | </syntaxhighlight> | ||
แถว 234: | แถว 228: | ||
self.y += MOVEMENT_SPEED * DIR_OFFSETS[direction][1] | self.y += MOVEMENT_SPEED * DIR_OFFSETS[direction][1] | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | Try to run the game. If the speed is too fast or too slow, you can change the constant, but make sure that it divides 40. This will be important later. | ||
{{gitcomment|Don't forget to commit.}} | {{gitcomment|Don't forget to commit.}} | ||
แถว 250: | แถว 246: | ||
The code below shows <tt>Maze</tt> class in <tt>src/Maze.js</tt>. 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. | The code below shows <tt>Maze</tt> class in <tt>src/Maze.js</tt>. 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. | ||
− | {{synfile| | + | {{synfile|models.py}} |
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
class Maze: | class Maze: | ||
แถว 260: | แถว 256: | ||
'#.#.###.####.###.#.#', | '#.#.###.####.###.#.#', | ||
'#.#.#..........#.#.#', | '#.#.#..........#.#.#', | ||
− | '#.....###. ###.....#', | + | '#.....###..###.....#', |
'#.#.#..........#.#.#', | '#.#.#..........#.#.#', | ||
'#.#.###.####.###.#.#', | '#.#.###.####.###.#.#', | ||
แถว 273: | แถว 269: | ||
We also put additional <tt>has_wall_at</tt> and <tt>has_dot_at</tt> methods for accessing maze information. | We also put additional <tt>has_wall_at</tt> and <tt>has_dot_at</tt> methods for accessing maze information. | ||
− | {{synfile| | + | {{synfile|models.py}} |
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
class Maze: | class Maze: | ||
แถว 287: | แถว 283: | ||
Let's create a maze object when we initialize the world. | Let's create a maze object when we initialize the world. | ||
− | {{synfile| | + | {{synfile|models.py}} |
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
class World: | class World: | ||
แถว 315: | แถว 311: | ||
for c in range(self.width): | for c in range(self.width): | ||
x = c * 40 + 20; | x = c * 40 + 20; | ||
− | y = | + | y = r * 40 + 60; |
if self.maze.has_wall_at(r,c): | if self.maze.has_wall_at(r,c): | ||
แถว 343: | แถว 339: | ||
We are ready to see our map. Let's load the game. | We are ready to see our map. Let's load the game. | ||
+ | |||
+ | '''Notes:''' We actually show the maze up-side-down. We do not see it because our map is symmetric around the horizontal axis, however, if we can the maze constants, we can see it. | ||
{{gitcomment|Don't forget to commit this nice scene.}} | {{gitcomment|Don't forget to commit this nice scene.}} | ||
แถว 359: | แถว 357: | ||
def get_sprite_position(self, r, c): | def get_sprite_position(self, r, c): | ||
x = c * 40 + 20; | x = c * 40 + 20; | ||
− | y = 600 - (r * 40) - | + | y = 600 - (r * 40) - 60; |
+ | return x,y | ||
def draw(self): | def draw(self): | ||
แถว 369: | แถว 368: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | == Moving inside the maze == | + | Let's run the code to see if it still works. |
+ | |||
+ | We add another constant <tt>BLOCK_SIZE</tt> so that we can remove 40 and 20 from the code. We will also use <tt>SCREEN_HEIGHT</tt> instead of 600. | ||
+ | |||
+ | {{synfile|maze.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | BLOCK_SIZE = 40 | ||
+ | |||
+ | # ... | ||
+ | class MazeDrawer(): | ||
+ | # ... | ||
+ | |||
+ | def get_sprite_position(self, r, c): | ||
+ | x = c * BLOCK_SIZE + (BLOCK_SIZE // 2); | ||
+ | y = r * BLOCK_SIZE + (BLOCK_SIZE + (BLOCK_SIZE // 2)); | ||
+ | return x,y | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Let's run again to make sure that we didn't break things. | ||
+ | |||
+ | Second improvement we can have is to remove duplicate codes for positioning and drawing sprites. Let's add method <tt>draw_sprite</tt> that takes the sprite, row <tt>r</tt> and column <tt>c</tt>. Note that we move the call to <tt>get_sprite_position</tt> into this method as well. | ||
+ | |||
+ | {{synfile|maze.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class MazeDrawer(): | ||
+ | # ... | ||
+ | |||
+ | def draw_sprite(self, sprite, r, c): | ||
+ | x, y = self.get_sprite_position(r, c) | ||
+ | sprite.set_position(x, y) | ||
+ | sprite.draw() | ||
+ | |||
+ | def draw(self): | ||
+ | for r in range(self.height): | ||
+ | for c in range(self.width): | ||
+ | if self.maze.has_wall_at(r,c): | ||
+ | self.draw_sprite(self.wall_sprite, r, c) | ||
+ | elif self.maze.has_dot_at(r,c): | ||
+ | self.draw_sprite(self.dot_sprite, r, c) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Let's run the code and if it works, | ||
+ | |||
+ | {{gitcomment|don't forget to commit your work.}} | ||
+ | |||
+ | ********************************************************************************* | ||
+ | ********************************* CHECK POINT 5.2 ******************************* | ||
+ | ********************************************************************************* | ||
+ | |||
+ | == Moving inside the maze (freely) == | ||
+ | |||
+ | Currently, our pacman moves freely inside the '''screen''' with no association with the maze we draw. We will make the connection in this part. | ||
+ | |||
+ | For the first step, we will constrain pacman's movements to be in block steps (in this case in block step of size 40). Note that to do so, we should let the model classes knows the constant BLOCK_SIZE, through the World class. | ||
+ | |||
+ | === Passing BLOCK_SIZE to the world and pacman === | ||
+ | Let's update the initialization of the World class. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | def __init__(self, width, height, block_size): | ||
+ | self.width = width | ||
+ | self.height = height | ||
+ | self.block_size = block_size | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Update the code where we create our world model. | ||
+ | |||
+ | {{synfile|maze.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class MazeWindow(arcade.Window): | ||
+ | def __init__(self, width, height): | ||
+ | # ... | ||
+ | |||
+ | self.world = World(SCREEN_WIDTH, SCREEN_HEIGHT, BLOCK_SIZE) | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | We also let the Pacman knows the block size. We will take this opportunity to let it knows the maze. | ||
+ | |||
+ | First, update the Pacman class. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Pacman: | ||
+ | def __init__(self, world, x, y, maze, block_size): | ||
+ | # ... | ||
+ | |||
+ | self.maze = maze | ||
+ | |||
+ | self.block_size = block_size | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Fix the creation of Pacman. '''We also move it to the position that is align with the maze.''' '''THIS IS IMPORTANT.''' (Can you see why?) | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | def __init__(self, width, height, block_size): | ||
+ | # ... | ||
+ | |||
+ | self.pacman = Pacman(self, 60, 100, | ||
+ | self.maze, | ||
+ | self.block_size) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === The (blocky) movement === | ||
+ | |||
+ | To ensure that pacman moves in block step, we will employ the following strategy: when the player changes direction, we will let the pacman continue to move in the old direction until it arrives at the center of the block. Then, we change its direction while it is at the center of the block. See figures below. | ||
+ | |||
+ | [[Image:Maze-movement-states.png]] | ||
+ | |||
+ | The movement will not be very smooth, but it is currently OK for us. (Or, for me.) | ||
+ | |||
+ | We need to add attribute <tt>next_direction</tt> to keep the proposed next direction. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Pacman: | ||
+ | def __init__(self, world, x, y, maze, block_size): | ||
+ | # ... | ||
+ | |||
+ | self.next_direction = DIR_STILL | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | The following method checks if we are at the center of the block. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Pacman: | ||
+ | # ... | ||
+ | |||
+ | def is_at_center(self): | ||
+ | half_size = self.block_size // 2 | ||
+ | return (((self.x - half_size) % self.block_size == 0) and | ||
+ | ((self.y - half_size) % self.block_size == 0)) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | We only update Pacman's direction when it is at the center. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Pacman: | ||
+ | # ... | ||
+ | |||
+ | def update(self, delta): | ||
+ | if self.is_at_center(): | ||
+ | self.direction = self.next_direction | ||
+ | self.move(self.direction) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | It's time to fix the World class to update Pacman's direction through its next_direction. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | # ... | ||
+ | |||
+ | def on_key_press(self, key, key_modifiers): | ||
+ | if key == arcade.key.UP: | ||
+ | self.pacman.next_direction = DIR_UP | ||
+ | # TODO: do the same for every other directions | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Try to run the code to see if our Pacman moves in the way that we intend. If that's the case, | ||
+ | |||
+ | {{gitcomment|commit your code.}} | ||
+ | |||
+ | === Key map === | ||
+ | |||
+ | The if statements in <tt>on_key_press</tt> is fairly annoying. We will create a dictionary that maps the key to the direction and use it instead. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | KEY_MAP = { arcade.key.UP: DIR_UP, | ||
+ | arcade.key.DOWN: DIR_DOWN, | ||
+ | arcade.key.LEFT: DIR_LEFT, | ||
+ | arcade.key.RIGHT: DIR_RIGHT, } | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | And change <tt>on_key_press</tt> to | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | # ... | ||
+ | |||
+ | def on_key_press(self, key, key_modifiers): | ||
+ | if key in KEY_MAP: | ||
+ | self.pacman.next_direction = KEY_MAP[key] | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Test and | ||
+ | |||
+ | {{gitcomment|commit your code.}} | ||
+ | |||
+ | ********************************************************************************* | ||
+ | ********************************* CHECK POINT 5.3 ******************************* | ||
+ | ********************************************************************************* | ||
+ | |||
+ | == The pacman's update plan == | ||
+ | |||
+ | Let's try to think ahead a bit. Our Pacman will move, but it should not move into the wall. It also eats dot. All these behaviors will be implemented inside the update loop. Let's add empty functions for doing that to see how we would do it. | ||
+ | |||
+ | We add two empty methods <tt>check_walls</tt> and <tt>check_dots</tt>. Method <tt>check_walls</tt> will look at the maze and if the moving direction goes into the wall, we will change the direction to <tt>DIR_STILL</tt>. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Pacman: | ||
+ | # ... | ||
+ | |||
+ | def check_walls(self): | ||
+ | return True | ||
+ | |||
+ | def check_dots(self): | ||
+ | pass | ||
+ | |||
+ | def update(self, delta): | ||
+ | if self.is_at_center(): | ||
+ | if self.check_walls(self.next_direction): | ||
+ | self.direction = self.next_direction | ||
+ | else: | ||
+ | self.direction = DIR_STILL | ||
+ | |||
+ | self.move(self.direction) | ||
+ | |||
+ | if self.is_at_center(): | ||
+ | self.check_dots() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Note that the update method gets a bit more complex, but it is still fairly readable. | ||
+ | |||
+ | == Exercise: moving while avoiding the walls == | ||
+ | |||
+ | To implement method <tt>check_walls</tt>, we need to be able to find out the current location in the maze of Pacman (in terms of rows and columns, not x/y). They are as follow. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Pacman: | ||
+ | # ... | ||
+ | def get_row(self): | ||
+ | return (self.y - self.block_size) // self.block_size | ||
+ | |||
+ | def get_col(self): | ||
+ | return self.x // self.block_size | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | From that you should be able to implement this function. (Hint: use DIR_OFFSET to find <tt>new_r</tt> and <tt>new_c</tt>) | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Pacman: | ||
+ | # ... | ||
+ | def check_walls(self, direction): | ||
+ | new_r = _______________________________________ | ||
+ | new_c = _______________________________________ | ||
+ | return not self.maze.has_wall_at(new_r, new_c) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Test and | ||
+ | |||
+ | {{gitcomment|commit your code.}} | ||
+ | |||
+ | ********************************************************************************* | ||
+ | ********************************* CHECK POINT 5.4 ******************************* | ||
+ | ********************************************************************************* | ||
+ | |||
+ | == Exercise: Eating Dots == | ||
+ | === Maintaining dot information === | ||
+ | Methods <tt>get_row</tt> and <tt>get_col</tt> will be useful to implement this part. We will start by considering how to keep dot information. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Maze: | ||
+ | def __init__(self, world): | ||
+ | # ... | ||
+ | |||
+ | self.init_dot_data() | ||
+ | |||
+ | def init_dot_data(self): | ||
+ | has_dot = {} | ||
+ | for r in range(self.height): | ||
+ | has_dot[r] = {} | ||
+ | for c in range(self.width): | ||
+ | has_dot[r][c] = self.map[r][c] == '.' | ||
+ | self.has_dot = has_dot | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | def has_dot_at(self, r, c): | ||
+ | return self.has_dot[r][c] | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Exercise: Updating dots === | ||
+ | |||
+ | We add another method <tt>remove_dot_at</tt> at Maze. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Maze: | ||
+ | # ... | ||
+ | def remove_dot_at(self, r, c): | ||
+ | self.has_dot[r][c] = False | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | You have to implement the following method that checks if there is a dot at the current location. If there's a dot, remove it. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class Pacman: | ||
+ | # ... | ||
+ | def check_dots(self): | ||
+ | _____________________________ | ||
+ | _____________________________ | ||
+ | _____________________________ | ||
+ | _____________________________ | ||
+ | _____________________________ | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Test and | ||
+ | |||
+ | {{gitcomment|commit your code.}} | ||
+ | |||
+ | ********************************************************************************* | ||
+ | ********************************* CHECK POINT 5.5 ******************************* | ||
+ | ********************************************************************************* | ||
+ | |||
+ | == Scores == | ||
+ | |||
+ | We will let the world keeps your score, which is initialized to be 0. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | def __init__(self, width, height, block_size): | ||
+ | # ... | ||
+ | |||
+ | self.score = 0 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | The World will provide method <tt>increase_score</tt> so that Pacman can notify it when you can eat a dot. | ||
+ | |||
+ | {{synfile|models.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class World: | ||
+ | # ... | ||
+ | def increase_score(self): | ||
+ | self.score += 1 | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Displaying score (in MazeWindow) === | ||
+ | |||
+ | MazeWindow will read the score from world and show it. | ||
+ | |||
+ | {{synfile|maze.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class MazeWindow(arcade.Window): | ||
+ | # ... | ||
+ | |||
+ | def on_draw(self): | ||
+ | # ... | ||
+ | arcade.draw_text(str(self.world.score), | ||
+ | self.width - 60, self.height - 30, | ||
+ | arcade.color.WHITE, 20) | ||
+ | |||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Exercise: update score === | ||
+ | |||
+ | '''Exercise:''' To get the score updated, you have to modify Pacman to notify the world when Pacman eats the dot. | ||
+ | |||
+ | {{gitcomment|Don't forget to commit your code.}} | ||
+ | |||
+ | ********************************************************************************* | ||
+ | ********************************* CHECK POINT 5.6 ******************************* | ||
+ | ********************************************************************************* | ||
+ | |||
+ | == More features == | ||
+ | |||
+ | Currently it is not really a game, since there is no challenge. Add more challenge to the game. |
รุ่นแก้ไขปัจจุบันเมื่อ 06:52, 1 มีนาคม 2562
- 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.
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.BLACK)
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.
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
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.
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.
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.
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.
Movement
Models
We start by adding direction constants as in the snake game at the top of 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.
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 We will not handle key releases because we want Pacman to keep moving even after we release the key.
To deal with key events, we first wire the on_key_press from the window to the world.
class MazeWindow(arcade.Window):
# ...
def on_key_press(self, key, key_modifiers):
self.world.on_key_press(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:
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.
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
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.
Faster
The pacman moves fairly slowly. Let's add a default speed, by adding another constants in models.py.
MOVEMENT_SPEED = 4
And update Pacman.move to use this constant.
class Pacman:
# ...
def move(self, direction):
self.x += MOVEMENT_SPEED * DIR_OFFSETS[direction][0]
self.y += MOVEMENT_SPEED * DIR_OFFSETS[direction][1]
Try to run the game. If the speed is too fast or too slow, you can change the constant, but make sure that it divides 40. This will be important later.
********************************************************************************* ********************************* 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.
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.
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.
class World:
def __init__(self, width, height):
# ...
self.maze = Maze(self)
MazeDrawer
To draw the maze, we create class MazeDrawer in maze.py Note the way we calculate positions for each wall block and dot block.
class MazeDrawer():
def __init__(self, maze):
self.maze = maze
self.width = self.maze.width
self.height = self.maze.height
self.wall_sprite = arcade.Sprite('images/wall.png')
self.dot_sprite = arcade.Sprite('images/dot.png')
def draw(self):
for r in range(self.height):
for c in range(self.width):
x = c * 40 + 20;
y = r * 40 + 60;
if self.maze.has_wall_at(r,c):
self.wall_sprite.set_position(x,y)
self.wall_sprite.draw()
elif self.maze.has_dot_at(r,c):
self.dot_sprite.set_position(x,y)
self.dot_sprite.draw()
Add this drawer into MazeWindow and call it in MazeWindow.draw.
class MazeWindow(arcade.Window):
def __init__(self, width, height):
# ...
self.maze_drawer = MazeDrawer(self.world.maze)
def on_draw(self):
arcade.start_render()
self.maze_drawer.draw() # make sure you call this before drawing pacman sprite
self.pacman_sprite.draw()
We are ready to see our map. Let's load the game.
Notes: We actually show the maze up-side-down. We do not see it because our map is symmetric around the horizontal axis, however, if we can the maze constants, we can see it.
Cleaning up MazeDrawer
While our code works, there are many improvements we can do before we move on to other features.
First, when we calculate x and y in draw method, we use a lot of magic numbers. Let's extract this code to a function and get rid of magic numbers.
We extract the code into get_sprite_position
class MazeDrawer():
# ...
def get_sprite_position(self, r, c):
x = c * 40 + 20;
y = 600 - (r * 40) - 60;
return x,y
def draw(self):
for r in range(self.height):
for c in range(self.width):
x,y = self.get_sprite_position(r,c)
# ...
Let's run the code to see if it still works.
We add another constant BLOCK_SIZE so that we can remove 40 and 20 from the code. We will also use SCREEN_HEIGHT instead of 600.
BLOCK_SIZE = 40
# ...
class MazeDrawer():
# ...
def get_sprite_position(self, r, c):
x = c * BLOCK_SIZE + (BLOCK_SIZE // 2);
y = r * BLOCK_SIZE + (BLOCK_SIZE + (BLOCK_SIZE // 2));
return x,y
Let's run again to make sure that we didn't break things.
Second improvement we can have is to remove duplicate codes for positioning and drawing sprites. Let's add method draw_sprite that takes the sprite, row r and column c. Note that we move the call to get_sprite_position into this method as well.
class MazeDrawer():
# ...
def draw_sprite(self, sprite, r, c):
x, y = self.get_sprite_position(r, c)
sprite.set_position(x, y)
sprite.draw()
def draw(self):
for r in range(self.height):
for c in range(self.width):
if self.maze.has_wall_at(r,c):
self.draw_sprite(self.wall_sprite, r, c)
elif self.maze.has_dot_at(r,c):
self.draw_sprite(self.dot_sprite, r, c)
Let's run the code and if it works,
********************************************************************************* ********************************* CHECK POINT 5.2 ******************************* *********************************************************************************
Moving inside the maze (freely)
Currently, our pacman moves freely inside the screen with no association with the maze we draw. We will make the connection in this part.
For the first step, we will constrain pacman's movements to be in block steps (in this case in block step of size 40). Note that to do so, we should let the model classes knows the constant BLOCK_SIZE, through the World class.
Passing BLOCK_SIZE to the world and pacman
Let's update the initialization of the World class.
class World:
def __init__(self, width, height, block_size):
self.width = width
self.height = height
self.block_size = block_size
# ...
Update the code where we create our world model.
class MazeWindow(arcade.Window):
def __init__(self, width, height):
# ...
self.world = World(SCREEN_WIDTH, SCREEN_HEIGHT, BLOCK_SIZE)
# ...
We also let the Pacman knows the block size. We will take this opportunity to let it knows the maze.
First, update the Pacman class.
class Pacman:
def __init__(self, world, x, y, maze, block_size):
# ...
self.maze = maze
self.block_size = block_size
Fix the creation of Pacman. We also move it to the position that is align with the maze. THIS IS IMPORTANT. (Can you see why?)
class World:
def __init__(self, width, height, block_size):
# ...
self.pacman = Pacman(self, 60, 100,
self.maze,
self.block_size)
The (blocky) movement
To ensure that pacman moves in block step, we will employ the following strategy: when the player changes direction, we will let the pacman continue to move in the old direction until it arrives at the center of the block. Then, we change its direction while it is at the center of the block. See figures below.
The movement will not be very smooth, but it is currently OK for us. (Or, for me.)
We need to add attribute next_direction to keep the proposed next direction.
class Pacman:
def __init__(self, world, x, y, maze, block_size):
# ...
self.next_direction = DIR_STILL
The following method checks if we are at the center of the block.
class Pacman:
# ...
def is_at_center(self):
half_size = self.block_size // 2
return (((self.x - half_size) % self.block_size == 0) and
((self.y - half_size) % self.block_size == 0))
We only update Pacman's direction when it is at the center.
class Pacman:
# ...
def update(self, delta):
if self.is_at_center():
self.direction = self.next_direction
self.move(self.direction)
It's time to fix the World class to update Pacman's direction through its next_direction.
class World:
# ...
def on_key_press(self, key, key_modifiers):
if key == arcade.key.UP:
self.pacman.next_direction = DIR_UP
# TODO: do the same for every other directions
Try to run the code to see if our Pacman moves in the way that we intend. If that's the case,
Key map
The if statements in on_key_press is fairly annoying. We will create a dictionary that maps the key to the direction and use it instead.
KEY_MAP = { arcade.key.UP: DIR_UP,
arcade.key.DOWN: DIR_DOWN,
arcade.key.LEFT: DIR_LEFT,
arcade.key.RIGHT: DIR_RIGHT, }
And change on_key_press to
class World:
# ...
def on_key_press(self, key, key_modifiers):
if key in KEY_MAP:
self.pacman.next_direction = KEY_MAP[key]
Test and
********************************************************************************* ********************************* CHECK POINT 5.3 ******************************* *********************************************************************************
The pacman's update plan
Let's try to think ahead a bit. Our Pacman will move, but it should not move into the wall. It also eats dot. All these behaviors will be implemented inside the update loop. Let's add empty functions for doing that to see how we would do it.
We add two empty methods check_walls and check_dots. Method check_walls will look at the maze and if the moving direction goes into the wall, we will change the direction to DIR_STILL.
class Pacman:
# ...
def check_walls(self):
return True
def check_dots(self):
pass
def update(self, delta):
if self.is_at_center():
if self.check_walls(self.next_direction):
self.direction = self.next_direction
else:
self.direction = DIR_STILL
self.move(self.direction)
if self.is_at_center():
self.check_dots()
Note that the update method gets a bit more complex, but it is still fairly readable.
Exercise: moving while avoiding the walls
To implement method check_walls, we need to be able to find out the current location in the maze of Pacman (in terms of rows and columns, not x/y). They are as follow.
class Pacman:
# ...
def get_row(self):
return (self.y - self.block_size) // self.block_size
def get_col(self):
return self.x // self.block_size
From that you should be able to implement this function. (Hint: use DIR_OFFSET to find new_r and new_c)
class Pacman:
# ...
def check_walls(self, direction):
new_r = _______________________________________
new_c = _______________________________________
return not self.maze.has_wall_at(new_r, new_c)
Test and
********************************************************************************* ********************************* CHECK POINT 5.4 ******************************* *********************************************************************************
Exercise: Eating Dots
Maintaining dot information
Methods get_row and get_col will be useful to implement this part. We will start by considering how to keep dot information.
class Maze:
def __init__(self, world):
# ...
self.init_dot_data()
def init_dot_data(self):
has_dot = {}
for r in range(self.height):
has_dot[r] = {}
for c in range(self.width):
has_dot[r][c] = self.map[r][c] == '.'
self.has_dot = has_dot
def has_dot_at(self, r, c):
return self.has_dot[r][c]
Exercise: Updating dots
We add another method remove_dot_at at Maze.
class Maze:
# ...
def remove_dot_at(self, r, c):
self.has_dot[r][c] = False
You have to implement the following method that checks if there is a dot at the current location. If there's a dot, remove it.
class Pacman:
# ...
def check_dots(self):
_____________________________
_____________________________
_____________________________
_____________________________
_____________________________
Test and
********************************************************************************* ********************************* CHECK POINT 5.5 ******************************* *********************************************************************************
Scores
We will let the world keeps your score, which is initialized to be 0.
class World:
def __init__(self, width, height, block_size):
# ...
self.score = 0
The World will provide method increase_score so that Pacman can notify it when you can eat a dot.
class World:
# ...
def increase_score(self):
self.score += 1
Displaying score (in MazeWindow)
MazeWindow will read the score from world and show it.
class MazeWindow(arcade.Window):
# ...
def on_draw(self):
# ...
arcade.draw_text(str(self.world.score),
self.width - 60, self.height - 30,
arcade.color.WHITE, 20)
Exercise: update score
Exercise: To get the score updated, you have to modify Pacman to notify the world when Pacman eats the dot.
********************************************************************************* ********************************* CHECK POINT 5.6 ******************************* *********************************************************************************
More features
Currently it is not really a game, since there is no challenge. Add more challenge to the game.