ผลต่างระหว่างรุ่นของ "Prg2/design patterns 1"
ไปยังการนำทาง
ไปยังการค้นหา
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) (→Codes) |
||
(ไม่แสดง 6 รุ่นระหว่างกลางโดยผู้ใช้คนเดียวกัน) | |||
แถว 11: | แถว 11: | ||
** State: [https://en.wikipedia.org/wiki/State_pattern wikipedia], [https://gameprogrammingpatterns.com/state.html game programmming patterns] | ** State: [https://en.wikipedia.org/wiki/State_pattern wikipedia], [https://gameprogrammingpatterns.com/state.html game programmming patterns] | ||
** Singleton: [https://en.wikipedia.org/wiki/Singleton_pattern wikipedia], [https://gameprogrammingpatterns.com/singleton.html game programmming patterns] | ** Singleton: [https://en.wikipedia.org/wiki/Singleton_pattern wikipedia], [https://gameprogrammingpatterns.com/singleton.html game programmming patterns] | ||
+ | |||
+ | == Clips == | ||
+ | |||
+ | * Design Patterns Part 1: [https://www.youtube.com/watch?v=iFaOXeF4HNs youtube] | ||
+ | * Design Patterns Part 2: [https://www.youtube.com/watch?v=_Kehl6n9JFA youtube] | ||
+ | * Design Patterns Part 3: [https://www.youtube.com/watch?v=lb2RUVzFEX8 youtube] | ||
== Codes == | == Codes == | ||
− | === Observer Pattern === | + | === Observer Pattern (OO version) === |
+ | {{synfile|gamelib.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class GameApp(ttk.Frame): | ||
+ | def __init__(self, parent, canvas_width=800, canvas_height=500, update_delay=33): | ||
+ | # ... | ||
+ | self.on_key_pressed_observers = [] | ||
+ | |||
+ | # ... | ||
+ | |||
+ | def register_on_key_pressed_observer(self, observer): | ||
+ | self.on_key_pressed_observers.append(observer) | ||
+ | |||
+ | def on_key_pressed(self, event): | ||
+ | for observer in self.on_key_pressed_observers: | ||
+ | observer.notify(event) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | {{synfile|monkeys.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class MonkeyGame(GameApp): | ||
+ | class AppObserver: | ||
+ | def __init__(self, app): | ||
+ | self.app = app | ||
+ | |||
+ | class SpeedAdjustmentObserver(AppObserver): | ||
+ | def notify(self, event): | ||
+ | app = self.app | ||
+ | if event.char == '+': | ||
+ | if app.speed < 10: | ||
+ | app.speed += 1 | ||
+ | app.update_speed_text() | ||
+ | |||
+ | if event.char == '-': | ||
+ | if app.speed > 1: | ||
+ | app.speed -= 1 | ||
+ | app.update_speed_text() | ||
+ | |||
+ | class BananaThrowingObserver(AppObserver): | ||
+ | def notify(self, event): | ||
+ | app = self.app | ||
+ | if event.char == ' ': | ||
+ | if not app.banana.is_moving: | ||
+ | app.banana.set_speed(3 * app.speed, 5 * app.speed) | ||
+ | app.banana.reset() | ||
+ | app.banana.start() | ||
+ | |||
+ | # ... | ||
+ | |||
+ | def init_game(self): | ||
+ | # ... | ||
+ | |||
+ | self.register_on_key_pressed_observer(MonkeyGame.SpeedAdjustmentObserver(self)) | ||
+ | self.register_on_key_pressed_observer(MonkeyGame.BananaThrowingObserver(self)) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Observer Pattern (functions) === | ||
+ | {{synfile|gamelib.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class GameApp(ttk.Frame): | ||
+ | def __init__(self, parent, canvas_width=800, canvas_height=500, update_delay=33): | ||
+ | # ... | ||
+ | |||
+ | self.on_key_pressed_handlers = [] | ||
+ | |||
+ | # ... | ||
+ | |||
+ | def register_on_key_pressed_handler(self, f): | ||
+ | self.on_key_pressed_handlers.append(f) | ||
+ | |||
+ | def on_key_pressed(self, event): | ||
+ | for f in self.on_key_pressed_handlers: | ||
+ | f(event) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | {{synfile|monkeys.py}} | ||
+ | <syntaxhighlight lang="python"> | ||
+ | class MonkeyGame(GameApp): | ||
+ | def handle_speed_adjustment_key_pressed(self, event): | ||
+ | if event.char == '+': | ||
+ | if self.speed < 10: | ||
+ | self.speed += 1 | ||
+ | self.update_speed_text() | ||
+ | |||
+ | if event.char == '-': | ||
+ | if self.speed > 1: | ||
+ | self.speed -= 1 | ||
+ | self.update_speed_text() | ||
+ | |||
+ | def handle_banana_throwing_key_pressed(self, event): | ||
+ | if event.char == ' ': | ||
+ | if not self.banana.is_moving: | ||
+ | self.banana.set_speed(3 * self.speed, 5 * self.speed) | ||
+ | self.banana.reset() | ||
+ | self.banana.start() | ||
+ | # ... | ||
+ | |||
+ | def init_game(self): | ||
+ | # ... | ||
+ | |||
+ | self.register_on_key_pressed_handler(self.handle_speed_adjustment_key_pressed) | ||
+ | self.register_on_key_pressed_handler(self.handle_banana_throwing_key_pressed) | ||
+ | </syntaxhighlight> | ||
=== Command Pattern === | === Command Pattern === | ||
+ | <syntaxhighlight lang="python"> | ||
+ | # ... | ||
+ | |||
+ | class DotUpdateCommand: | ||
+ | def __init__(self, dot): | ||
+ | self.dot = dot | ||
+ | |||
+ | def excute(self): | ||
+ | self.old_state = self.dot.get_state() | ||
+ | self.dot.real_update() | ||
+ | |||
+ | def undo(self): | ||
+ | self.dot.set_state(self.old_state) | ||
+ | |||
+ | class Dot(Sprite): | ||
+ | def init_element(self): | ||
+ | self.vx = 0 | ||
+ | self.vy = 0 | ||
+ | |||
+ | def random_speed(self): | ||
+ | self.vx = 5 * randint(-5,5) | ||
+ | self.vy = -5 * randint(1,10) | ||
+ | |||
+ | def bounce(self): | ||
+ | if (self.x > CANVAS_WIDTH) or (self.x < 0): | ||
+ | self.vx = -self.vx | ||
+ | |||
+ | if self.y > CANVAS_HEIGHT: | ||
+ | self.vy = -0.85 * self.vy | ||
+ | |||
+ | def real_update(self): | ||
+ | self.x += self.vx | ||
+ | self.y += self.vy | ||
+ | self.vy += GRAVITY | ||
+ | |||
+ | self.bounce() | ||
+ | |||
+ | def get_update_command(self): | ||
+ | return DotUpdateCommand(self) | ||
+ | |||
+ | def get_state(self): | ||
+ | return (self.x, self.y, self.vx, self.vy) | ||
+ | |||
+ | def set_state(self, state): | ||
+ | self.x, self.y, self.vx, self.vy = state | ||
+ | |||
+ | class FlappyGame(GameApp): | ||
+ | def create_sprites(self): | ||
+ | self.dots = [] | ||
+ | for i in range(NUM_BALLS): | ||
+ | dot = Dot(self, 'images/dot.png', CANVAS_WIDTH // 2, CANVAS_HEIGHT // 2) | ||
+ | dot.random_speed() | ||
+ | |||
+ | self.dots.append(dot) | ||
+ | self.elements.append(dot) | ||
+ | |||
+ | def init_game(self): | ||
+ | self.create_sprites() | ||
+ | |||
+ | self.commands = [] | ||
+ | self.is_reversed = False | ||
+ | self.cmd_index = 0 | ||
+ | |||
+ | def reverse_update(self): | ||
+ | current_commands = self.commands[self.cmd_index] | ||
+ | |||
+ | for c in reversed(current_commands): | ||
+ | c.undo() | ||
+ | |||
+ | self.cmd_index -= 1 | ||
+ | if self.cmd_index < 0: | ||
+ | self.is_reversed = False | ||
+ | self.commands = [] | ||
+ | |||
+ | def create_update_commands(self): | ||
+ | new_commands = [] | ||
+ | for dot in self.dots: | ||
+ | new_commands.append(dot.get_update_command()) | ||
+ | return new_commands | ||
+ | |||
+ | def pre_update(self): | ||
+ | if self.is_reversed: | ||
+ | self.reverse_update() | ||
+ | return | ||
+ | |||
+ | new_commands = self.create_update_commands() | ||
+ | for c in new_commands: | ||
+ | c.execute() | ||
+ | |||
+ | self.commands.append(new_commands) | ||
+ | self.cmd_index = len(self.commands) - 1 | ||
+ | |||
+ | def on_key_pressed(self, event): | ||
+ | self.is_reversed = True | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Boucing ball code: [https://github.com/jittat/bouncing-balls-command-pattern github] | ||
=== State Pattern === | === State Pattern === | ||
+ | <syntaxhighlight lang="python"> | ||
+ | import math | ||
+ | |||
+ | # ... | ||
+ | |||
+ | SPEED = 10 | ||
+ | |||
+ | DIR_UP = 1 | ||
+ | DIR_RIGHT = 2 | ||
+ | DIR_DOWN = 3 | ||
+ | DIR_LEFT = 4 | ||
+ | |||
+ | DIR_OFFSETS = { | ||
+ | DIR_UP: (0,-1), | ||
+ | DIR_RIGHT: (1,0), | ||
+ | DIR_DOWN: (0,1), | ||
+ | DIR_LEFT: (-1,0), | ||
+ | } | ||
+ | |||
+ | class DotNormalState: | ||
+ | def __init__(self, dot): | ||
+ | self.dot = dot | ||
+ | |||
+ | def update(self): | ||
+ | dot = self.dot | ||
+ | dot.x += SPEED * DIR_OFFSETS[dot.direction][0] | ||
+ | dot.y += SPEED * DIR_OFFSETS[dot.direction][1] | ||
+ | dot.wrap_position() | ||
+ | |||
+ | class DotExcitedState: | ||
+ | def __init__(self, dot): | ||
+ | self.dot = dot | ||
+ | self.counter = 0 | ||
+ | |||
+ | def update(self): | ||
+ | dot = self.dot | ||
+ | dot.x += 3 * SPEED * DIR_OFFSETS[dot.direction][0] | ||
+ | dot.y += 3 * SPEED * DIR_OFFSETS[dot.direction][1] | ||
+ | dot.wrap_position() | ||
+ | |||
+ | self.counter += 1 | ||
+ | |||
+ | if self.counter == 100: | ||
+ | dot.state = DotNormalState(dot) | ||
+ | |||
+ | class RedDot(Sprite): | ||
+ | instance = None | ||
+ | |||
+ | @staticmethod | ||
+ | def get_instance(app): | ||
+ | if not RedDot.instance: | ||
+ | RedDot.instance = RedDot(app, 'images/reddot.png') | ||
+ | return RedDot.instance | ||
+ | |||
+ | def random_position(self): | ||
+ | self.x = randint(20, CANVAS_WIDTH - 20) | ||
+ | self.y = randint(20, CANVAS_HEIGHT - 20) | ||
+ | |||
+ | |||
+ | class Dot(Sprite): | ||
+ | def init_element(self): | ||
+ | self.direction = DIR_UP | ||
+ | self.state = DotNormalState(self) | ||
+ | |||
+ | def wrap_position(self): | ||
+ | if self.x < 0: | ||
+ | self.x = CANVAS_WIDTH | ||
+ | if self.x > CANVAS_WIDTH: | ||
+ | self.x = 0 | ||
+ | if self.y < 0: | ||
+ | self.y = CANVAS_HEIGHT | ||
+ | if self.y > CANVAS_HEIGHT: | ||
+ | self.y = 0 | ||
+ | |||
+ | def get_excited(self): | ||
+ | self.state = DotExcitedState(self) | ||
+ | |||
+ | def update(self): | ||
+ | self.state.update() | ||
+ | |||
+ | def is_close_to(self, dot): | ||
+ | dist = math.sqrt((self.x - dot.x) ** 2 + (self.y - dot.y) ** 2) | ||
+ | return dist < 10 | ||
+ | |||
+ | |||
+ | class FlappyGame(GameApp): | ||
+ | def create_sprites(self): | ||
+ | self.dot = Dot(self, 'images/dot.png', CANVAS_WIDTH // 2, CANVAS_HEIGHT // 2) | ||
+ | |||
+ | self.red_dot = RedDot.get_instance(self) | ||
+ | self.red_dot.random_position() | ||
+ | |||
+ | self.elements.append(self.dot) | ||
+ | self.elements.append(self.red_dot) | ||
+ | |||
+ | def init_game(self): | ||
+ | self.create_sprites() | ||
+ | |||
+ | def post_update(self): | ||
+ | if self.dot.is_close_to(self.red_dot): | ||
+ | self.dot.get_excited() | ||
+ | |||
+ | self.red_dot.random_position() | ||
+ | |||
+ | def on_key_pressed(self, event): | ||
+ | if self.dot.direction == DIR_UP: | ||
+ | self.dot.direction = DIR_RIGHT | ||
+ | else: | ||
+ | self.dot.direction = DIR_UP | ||
+ | |||
+ | # ... | ||
+ | </syntaxhighlight> |
รุ่นแก้ไขปัจจุบันเมื่อ 02:40, 16 มีนาคม 2564
- This is part of Programming 2 2563
เนื้อหา
Basic information
- Game Programming Patterns by Robert Nystrom
- Refactoring.Guru
- Patterns covered this week
- Observer: wikipedia, game programmming patterns
- Command: wikipedia, game programmming patterns
- State: wikipedia, game programmming patterns
- Singleton: wikipedia, game programmming patterns
Clips
Codes
Observer Pattern (OO version)
File: gamelib.py
class GameApp(ttk.Frame):
def __init__(self, parent, canvas_width=800, canvas_height=500, update_delay=33):
# ...
self.on_key_pressed_observers = []
# ...
def register_on_key_pressed_observer(self, observer):
self.on_key_pressed_observers.append(observer)
def on_key_pressed(self, event):
for observer in self.on_key_pressed_observers:
observer.notify(event)
File: monkeys.py
class MonkeyGame(GameApp):
class AppObserver:
def __init__(self, app):
self.app = app
class SpeedAdjustmentObserver(AppObserver):
def notify(self, event):
app = self.app
if event.char == '+':
if app.speed < 10:
app.speed += 1
app.update_speed_text()
if event.char == '-':
if app.speed > 1:
app.speed -= 1
app.update_speed_text()
class BananaThrowingObserver(AppObserver):
def notify(self, event):
app = self.app
if event.char == ' ':
if not app.banana.is_moving:
app.banana.set_speed(3 * app.speed, 5 * app.speed)
app.banana.reset()
app.banana.start()
# ...
def init_game(self):
# ...
self.register_on_key_pressed_observer(MonkeyGame.SpeedAdjustmentObserver(self))
self.register_on_key_pressed_observer(MonkeyGame.BananaThrowingObserver(self))
Observer Pattern (functions)
File: gamelib.py
class GameApp(ttk.Frame):
def __init__(self, parent, canvas_width=800, canvas_height=500, update_delay=33):
# ...
self.on_key_pressed_handlers = []
# ...
def register_on_key_pressed_handler(self, f):
self.on_key_pressed_handlers.append(f)
def on_key_pressed(self, event):
for f in self.on_key_pressed_handlers:
f(event)
File: monkeys.py
class MonkeyGame(GameApp):
def handle_speed_adjustment_key_pressed(self, event):
if event.char == '+':
if self.speed < 10:
self.speed += 1
self.update_speed_text()
if event.char == '-':
if self.speed > 1:
self.speed -= 1
self.update_speed_text()
def handle_banana_throwing_key_pressed(self, event):
if event.char == ' ':
if not self.banana.is_moving:
self.banana.set_speed(3 * self.speed, 5 * self.speed)
self.banana.reset()
self.banana.start()
# ...
def init_game(self):
# ...
self.register_on_key_pressed_handler(self.handle_speed_adjustment_key_pressed)
self.register_on_key_pressed_handler(self.handle_banana_throwing_key_pressed)
Command Pattern
# ...
class DotUpdateCommand:
def __init__(self, dot):
self.dot = dot
def excute(self):
self.old_state = self.dot.get_state()
self.dot.real_update()
def undo(self):
self.dot.set_state(self.old_state)
class Dot(Sprite):
def init_element(self):
self.vx = 0
self.vy = 0
def random_speed(self):
self.vx = 5 * randint(-5,5)
self.vy = -5 * randint(1,10)
def bounce(self):
if (self.x > CANVAS_WIDTH) or (self.x < 0):
self.vx = -self.vx
if self.y > CANVAS_HEIGHT:
self.vy = -0.85 * self.vy
def real_update(self):
self.x += self.vx
self.y += self.vy
self.vy += GRAVITY
self.bounce()
def get_update_command(self):
return DotUpdateCommand(self)
def get_state(self):
return (self.x, self.y, self.vx, self.vy)
def set_state(self, state):
self.x, self.y, self.vx, self.vy = state
class FlappyGame(GameApp):
def create_sprites(self):
self.dots = []
for i in range(NUM_BALLS):
dot = Dot(self, 'images/dot.png', CANVAS_WIDTH // 2, CANVAS_HEIGHT // 2)
dot.random_speed()
self.dots.append(dot)
self.elements.append(dot)
def init_game(self):
self.create_sprites()
self.commands = []
self.is_reversed = False
self.cmd_index = 0
def reverse_update(self):
current_commands = self.commands[self.cmd_index]
for c in reversed(current_commands):
c.undo()
self.cmd_index -= 1
if self.cmd_index < 0:
self.is_reversed = False
self.commands = []
def create_update_commands(self):
new_commands = []
for dot in self.dots:
new_commands.append(dot.get_update_command())
return new_commands
def pre_update(self):
if self.is_reversed:
self.reverse_update()
return
new_commands = self.create_update_commands()
for c in new_commands:
c.execute()
self.commands.append(new_commands)
self.cmd_index = len(self.commands) - 1
def on_key_pressed(self, event):
self.is_reversed = True
# ...
Boucing ball code: github
State Pattern
import math
# ...
SPEED = 10
DIR_UP = 1
DIR_RIGHT = 2
DIR_DOWN = 3
DIR_LEFT = 4
DIR_OFFSETS = {
DIR_UP: (0,-1),
DIR_RIGHT: (1,0),
DIR_DOWN: (0,1),
DIR_LEFT: (-1,0),
}
class DotNormalState:
def __init__(self, dot):
self.dot = dot
def update(self):
dot = self.dot
dot.x += SPEED * DIR_OFFSETS[dot.direction][0]
dot.y += SPEED * DIR_OFFSETS[dot.direction][1]
dot.wrap_position()
class DotExcitedState:
def __init__(self, dot):
self.dot = dot
self.counter = 0
def update(self):
dot = self.dot
dot.x += 3 * SPEED * DIR_OFFSETS[dot.direction][0]
dot.y += 3 * SPEED * DIR_OFFSETS[dot.direction][1]
dot.wrap_position()
self.counter += 1
if self.counter == 100:
dot.state = DotNormalState(dot)
class RedDot(Sprite):
instance = None
@staticmethod
def get_instance(app):
if not RedDot.instance:
RedDot.instance = RedDot(app, 'images/reddot.png')
return RedDot.instance
def random_position(self):
self.x = randint(20, CANVAS_WIDTH - 20)
self.y = randint(20, CANVAS_HEIGHT - 20)
class Dot(Sprite):
def init_element(self):
self.direction = DIR_UP
self.state = DotNormalState(self)
def wrap_position(self):
if self.x < 0:
self.x = CANVAS_WIDTH
if self.x > CANVAS_WIDTH:
self.x = 0
if self.y < 0:
self.y = CANVAS_HEIGHT
if self.y > CANVAS_HEIGHT:
self.y = 0
def get_excited(self):
self.state = DotExcitedState(self)
def update(self):
self.state.update()
def is_close_to(self, dot):
dist = math.sqrt((self.x - dot.x) ** 2 + (self.y - dot.y) ** 2)
return dist < 10
class FlappyGame(GameApp):
def create_sprites(self):
self.dot = Dot(self, 'images/dot.png', CANVAS_WIDTH // 2, CANVAS_HEIGHT // 2)
self.red_dot = RedDot.get_instance(self)
self.red_dot.random_position()
self.elements.append(self.dot)
self.elements.append(self.red_dot)
def init_game(self):
self.create_sprites()
def post_update(self):
if self.dot.is_close_to(self.red_dot):
self.dot.get_excited()
self.red_dot.random_position()
def on_key_pressed(self, event):
if self.dot.direction == DIR_UP:
self.dot.direction = DIR_RIGHT
else:
self.dot.direction = DIR_UP
# ...