ผลต่างระหว่างรุ่นของ "Prg2/space (applying design patterns)"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
แถว 232: | แถว 232: | ||
# TODO: extracted from method create_enemy_from_edge | # TODO: extracted from method create_enemy_from_edge | ||
</syntaxhighlight> | </syntaxhighlight> | ||
+ | |||
+ | With these two strategies in place, we can also clean up our enemy generation code (with probabilities). We create a list of strategies with their associated accumulative probabilities in <tt>init_game</tt> | ||
+ | |||
+ | <syntaxhighlight lang="python"> | ||
+ | class SpaceGame(GameApp): | ||
+ | def init_game(self): | ||
+ | # ... | ||
+ | |||
+ | self.enemy_creation_strategies = [ | ||
+ | (0.2, StarEnemyGenerationStrategy()), | ||
+ | (1.0, EdgeEnemyGenerationStrategy()) | ||
+ | ] | ||
+ | |||
+ | # ... | ||
+ | |||
+ | def create_enemies(self): | ||
+ | p = random() | ||
+ | |||
+ | for prob, strategy in self.enemy_creation_strategies: | ||
+ | if p < prob: | ||
+ | enemies = strategy.generate(self, self.ship) | ||
+ | break | ||
+ | |||
+ | for e in enemies: | ||
+ | self.add_enemy(e) | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | This way of specifying a list of strategies enables multiple sets of enemy creation strategies in the game. This would be extremely useful when implementing game levels. | ||
== The Chain of Responsibilities Pattern == | == The Chain of Responsibilities Pattern == |
รุ่นแก้ไขเมื่อ 23:36, 24 มีนาคม 2564
- This is part of Programming 2 2563
เนื้อหา
Overview
In this assignment, you will apply design patterns to the provided Space Fighter code. In this game you can turn the ship with Left and Right keys. You can fire bullets with Spacebar and applying bombs with the Z key. You have 30 bullets at a time and the bomb needs cool-down period (to recharge its power to 100%).
Understanding the current code
There are 4 main classes:
- SpaceGame
- Ship (in elements.py)
- Bullet and Enemy (in elements.py)
Changes in gamelib
Since we will have a lot of sprites flying around, it would cause the game huge delays if we keep out-of-scene sprites in the game. We add attribute to_be_deleted so that the sprite or the game app could signify the game loop that a sprite can be discarded and method delete to perform the actual deletion from the canvas. We also provide methods for stoping the game stop_animation and resuming the game resume_animation. Method animate becomes:
class GameApp(ttk.Frame):
# ...
def animate(self):
if not self.is_stopped:
self.pre_update()
remaining_elements = []
for element in self.elements:
element.update()
element.render()
if element.to_be_deleted:
element.delete()
else:
remaining_elements.append(element)
self.elements = remaining_elements
self.post_update()
self.after(self.update_delay, self.animate)
We also add a few helper methods for collision detection to Sprite. (Implementations are in utils.py)
class GameCanvasElement(GameElement):
# ...
def distance_to(self, element):
return distance(self.x, self.y, element.x, element.y)
def is_within_distance(self, element, d):
return self.distance_to(element) <= d
SpaceGame
SpaceGame is the main class. It maintains a Ship, a list of Enemies, and a list of Bullets.
- It keeps bullets and enemies in self.bullets and self.enemies. The reason these objects are kept outside the typical self.elements is to improve performance when performing collision detection.
- There are 2 mechanisms to generate enemies.
def create_enemy_star(self):
# ...
def create_enemy_from_edges(self):
# ...
def create_enemies(self):
if random() < 0.2:
enemies = self.create_enemy_star()
else:
enemies = self.create_enemy_from_edges()
for e in enemies:
self.add_enemy(e)
- It runs collision detection to detect bullet-enemy collisions and enemy-ship collisions.
- It handles keyboard events (both key-pressed and key-released events).
- It also deals with bomb capacity of the ship: detonates the bomb, maintains bomb cool-down periods, and shows/hides bomb radius after it is detonated.
Ship
Ship (code in elements.py) maintains ship movement. It cruises forward with constant speed. The user can only change the ship's direction by turning left and right. It also generate bullets with method fire:
def fire(self):
if self.app.bullet_count() >= MAX_NUM_BULLETS:
return
dx,dy = direction_to_dxdy(self.direction)
bullet = Bullet(self.app, self.x, self.y, dx * BULLET_BASE_SPEED, dy * BULLET_BASE_SPEED)
self.app.add_bullet(bullet)
This code leaves rooms for more creativity in creating bullets, e.g., you can let the ship fire many bullets or even having a weapon system in the game.
Bullet and Enemy
Both Bullet and Enemy are FixedDirectionSprites that move at constant speed in a fixed direction.
class FixedDirectionSprite(Sprite):
def __init__(self, app, image_filename, x, y, vx, vy):
super().__init__(app, image_filename, x, y)
self.vx = vx
self.vy = vy
def update(self):
self.x += self.vx
self.y += self.vy
if (self.x < 0) or (self.y < 0) or (self.x > CANVAS_WIDTH) or (self.y > CANVAS_HEIGHT):
self.to_be_deleted = True
However, you can definitely create more complex enemies that follow the ship.
Bombing mechanism
When the user press 'Z', if the bomb power is full, GameApp destroys all enemies within the radius of BOMB_RADIUS from the ship. It briefly displays a circle to show this radius. This functionality is a complete HACK to the code. See the code below where the create_oval is called and its deletion is scheduled directly with after method from ttk.Frame.
def bomb(self):
if self.bomb_power == BOMB_FULL_POWER:
self.bomb_power = 0
self.bomb_canvas_id = self.canvas.create_oval(
self.ship.x - BOMB_RADIUS,
self.ship.y - BOMB_RADIUS,
self.ship.x + BOMB_RADIUS,
self.ship.y + BOMB_RADIUS
)
self.after(200, lambda: self.canvas.delete(self.bomb_canvas_id))
for e in self.enemies:
if self.ship.distance_to(e) <= BOMB_RADIUS:
e.to_be_deleted = True
self.update_bomb_power_text()
Statuses
SpaceGame shows 3 status texts: the score, the power level of the bomb, and the currently level. You can see that the code for these 3 statuses are extremely similar.
class SpaceGame(GameApp):
def init_game(self):
# ...
self.level = 1
self.level_text = Text(self, '', 100, 580)
self.update_level_text()
self.score = 0
self.score_wait = 0
self.score_text = Text(self, '', 100, 20)
self.update_score_text()
self.bomb_power = BOMB_FULL_POWER
self.bomb_wait = 0
self.bomb_power_text = Text(self, '', 700, 20)
self.update_bomb_power_text()
# ...
def update_score_text(self):
self.score_text.set_text('Score: %d' % self.score)
def update_bomb_power_text(self):
self.bomb_power_text.set_text('Power: %d%%' % self.bomb_power)
def update_level_text(self):
self.level_text.set_text('Level: %d' % self.level)
def update_score(self):
self.score_wait += 1
if self.score_wait >= SCORE_WAIT:
self.score += 1
self.score_wait = 0
self.update_score_text()
def update_bomb_power(self):
self.bomb_wait += 1
if (self.bomb_wait >= BOMB_WAIT) and (self.bomb_power != BOMB_FULL_POWER):
self.bomb_power += 1
self.bomb_wait = 0
self.update_bomb_power_text()
Notes: the previous code use f-format strings for status text, but it might be easier to use old formatting approach if you want to extract this status texts as a new class.
Getting started
This is an individual assignment, but you should still use branches in git for managing your work. You should use the following template as a starter code.
- Template: tk-pacman
Since this is the second time you work on design patterns, the instructions would be less specific and you can use your judgement more freely to improve the code.
The Strategy Pattern
The first application of design pattern to our code is to use Strategy Pattern to improve enemy creation. While currently there are only two ways to create enemies, the game could be more interesting with more enemy creation patterns.
We first look at the enemy generation methods: create_enemy_star and create_enemy_from_edge. The codes requires the game and the ship, so we create the following interface for the strategy.
class EnemyGenerationStrategy(ABC):
@abstractmethod
def generate(self, space_game, ship):
pass
The two methods should be extracted into the following classes:
class StarEnemyGenerationStrategy(EnemyGenerationStrategy):
def generate(self, space_game, ship):
####
# TODO: extracted from method create_enemy_star
class EdgeEnemyGenerationStrategy(EnemyGenerationStrategy):
def generate(self, space_game, ship):
####
# TODO: extracted from method create_enemy_from_edge
With these two strategies in place, we can also clean up our enemy generation code (with probabilities). We create a list of strategies with their associated accumulative probabilities in init_game
class SpaceGame(GameApp):
def init_game(self):
# ...
self.enemy_creation_strategies = [
(0.2, StarEnemyGenerationStrategy()),
(1.0, EdgeEnemyGenerationStrategy())
]
# ...
def create_enemies(self):
p = random()
for prob, strategy in self.enemy_creation_strategies:
if p < prob:
enemies = strategy.generate(self, self.ship)
break
for e in enemies:
self.add_enemy(e)
# ...
This way of specifying a list of strategies enables multiple sets of enemy creation strategies in the game. This would be extremely useful when implementing game levels.