แผนการเรียน
/

OOP & SOLID
ในการพัฒนาเกมด้วย Pygame

สถาปัตยกรรมซอฟต์แวร์สำหรับนักพัฒนาเกม: จาก Spaghetti Code สู่ Structured Design
Week 11-13 Pygame Framework Design Patterns

ทำไมต้องใช้ OOP ในการทำเกม?

ปัญหาแบบเดิม (Procedural)

  • Global Variables: ตัวแปร `player_x`, `enemy_list` กระจายทั่วไฟล์
  • God Loop: `while True:` ยาว 500 บรรทัดที่ทำทุกอย่าง
  • Hard to Extend: การเพิ่มศัตรูแบบใหม่ต้องแก้ code เดิมหลายจุด

ทางออกด้วย OOP

  • Encapsulation: ตัวละครจัดการสถานะ (HP, Pos) ของตัวเอง
  • Polymorphism: ศัตรูทุกตัวมี method `update()` แต่พฤติกรรมต่างกัน
  • Maintenance: แยก Logic, Rendering และ Input ออกจากกัน

หัวใจของ Pygame: The Sprite Class

Pygame เตรียมคลาส pygame.sprite.Sprite มาให้แล้ว ซึ่งรองรับ OOP โดยธรรมชาติ

class GameObject(pygame.sprite.Sprite):
    def __init__(self):
        # ต้องเรียก super().__init__() เสมอ!
        super().__init__()
        # 2 สิ่งที่ขาดไม่ได้สำหรับ Sprite:
        self.image = pygame.Surface((32, 32)) # หน้าตา
        self.rect = self.image.get_rect()     # ตำแหน่งและขนาด

    def update(self):
        # ใส่ Logic การเคลื่อนที่ที่นี่
        pass

The Game Loop Pattern

รูปแบบมาตรฐานของการทำงานเกม แยกเป็น 3 ขั้นตอนวนซ้ำ

1. Input

รับค่าจากผู้เล่น

2. Update

คำนวณฟิสิกส์/สถานะ

3. Draw

วาดภาพขึ้นจอ

*ควรแยก 3 ส่วนนี้ออกจากกันให้ชัดเจนในแต่ละ Class (SRP)

SRP: Single Responsibility Principle

❌ การละเมิด SRP

class Player:
  def update(self):
    # รับ Input
    keys = pygame.key.get_pressed()
    # คำนวณฟิสิกส์
    self.rect.x += 5
    # เล่นเสียง
    pygame.mixer.Sound("jump.wav").play()

Player ทำทุกอย่าง: Input, Logic, Audio

✅ ทำตาม SRP

class Player(Sprite):
  def update(self, inputs):
    # สนใจแค่ Logic การขยับ
    self.rect.x += self.speed * inputs.x

class InputHandler:
  # จัดการ Input แยกต่างหาก

แยก InputHandler และ SoundManager ออกมา

OCP: Open/Closed Principle

"เปิดรับการขยาย (เพิ่มศัตรูใหม่) แต่ปิดการแก้ไข (ไม่ต้องแก้โค้ดลูปหลัก)"

ตัวอย่าง: ระบบ Enemy

Base Class (Abstract)

class Enemy(ABC):
    @abstractmethod
    def attack(self): pass

Extensions

class Zombie(Enemy):
    def attack(self): bite()

class Alien(Enemy):
    def attack(self): shoot_laser()
Game Loop: for e in enemies: e.attack()
(ทำงานได้กับศัตรูทุกประเภทโดยไม่ต้องแก้โค้ดลูป)

LSP: Liskov Substitution Principle

"คลาสลูกต้องแทนที่คลาสแม่ได้โดยเกมไม่พัง"

ตัวอย่างที่ผิด (Violation)

คลาส Turret (ป้อมปืน) สืบทอดจาก Enemy แต่ในเมธอด move() ดันโยน Error เพราะป้อมปืนเดินไม่ได้
--> ทำให้ Game Loop ที่เรียก enemy.move() พัง

วิธีแก้ไข (Solution)

แยก Interface: MovableEnemy และ StaticEnemy หรือใช้เทคนิค Composition (มี component Movement หรือไม่)

ISP: Interface Segregation

"อย่าบังคับให้ Game Object ต้อง Implement เมธอดที่ไม่ได้ใช้"

Fat Interface

class IGameObject:
  def update()
  def draw()
  def play_sound()
  def handle_input()
  def collide()

ต้นไม้ (Tree) ต้อง implement play_sound() หรอ?

Segregated

class IDrawable:
  def draw()

class IUpdatable:
  def update()

class Tree(IDrawable): ...

ต้นไม้เลือกเป็นแค่สิ่งที่วาดได้ (Drawable)

DIP: Dependency Inversion

ตัวอย่าง: ระบบควบคุม (Input System)

ตัวละคร (High Level) ไม่ควรผูกติดกับ Keyboard (Low Level) โดยตรง เพราะเราอาจจะอยากใช้ JoyStick ในอนาคต

class Player:
    # ผิด: ผูกกับ Pygame Keyboard โดยตรง (Tight Coupling)
    def handle_input(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_SPACE]: jump()

    # ถูก: รับ Command เข้ามา (Dependency Injection)
    def handle_input(self, command: ICommand):
        command.execute(self)

Composition Over Inheritance

Entity-Component Idea

แทนที่จะสร้าง Class ลึกๆ ให้สร้าง Object จากการประกอบชิ้นส่วน (Components)

  • Player = Sprite + Control + Health + Weapon
  • Bullet = Sprite + Velocity + Damage
class GameObject(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        # Has-A relationships
        self.transform = Transform()
        self.renderer = SpriteRenderer()
        self.audio = AudioSource()
        self.physics = RigidBody()

Pattern 1: Factory Method

ใช้สำหรับสร้าง Game Objects (Spawning) จำนวนมากและหลากหลาย

Enemy Spawner

แทนที่จะ new Zombie() กระจายไปทั่วโค้ด ให้ใช้ Factory จัดการ การสุ่มประเภทและตำแหน่งเกิด

class EnemyFactory:
    @staticmethod
    def create_enemy(type, x, y):
        if type == "ZOMBIE":
            return Zombie(x, y)
        elif type == "ALIEN":
            return Alien(x, y)
            
# Usage
enemies.add(EnemyFactory.create_enemy("ZOMBIE", 100, 200))

Pattern 2: Observer

จัดการ Events: ทำอย่างไรให้ HP Bar ลดลงเมื่อ Player โดนโจมตี โดยที่ Player ไม่ต้องรู้จัก HP Bar?

Subject (Player)
➡ Notify() ➡
Observer (UI HP Bar)
Observer (Sound System)
Observer (Achievement)

Player ทำหน้าที่แค่ notify_observers("DAMAGE") ใครที่ Subscribe อยู่ก็จะทำงานของตัวเอง (Decoupling)

Pattern 3: State Pattern

จัดการสถานะของเกม (Menu, Playing, Paused, GameOver)

GameState (Abstract)

  • handle_input()
  • update()
  • draw()
MenuState
PlayState
PauseState
GameOverState
ใน Game Loop หลัก เราเรียกแค่ current_state.update()
ตัวแปร current_state จะเปลี่ยนไปตามเหตุการณ์ในเกม

โครงสร้างโปรเจกต์ที่ดี (Best Practice)

my_game/
├── main.py # Entry Point
├── config.py # Constants
├── assets/ # Images/Sounds
├── src/
│ ├── __init__.py
│ ├── game.py # Game Manager
│ ├── entities/ # Player, Enemy
│ ├── systems/ # Input, Physics
│ └── utils/ # Helper funcs

✅ แยกไฟล์ตามหน้าที่ (Modularity)

✅ รวมค่าคงที่ไว้ที่เดียว (Config)

✅ แยก Assets ออกจาก Code

Managers & Controllers

คลาสที่ทำหน้าที่ควบคุมระบบย่อยต่างๆ (มักใช้ Singleton หรือ Static)

SpriteManager

จัดการ Group ของ Sprite ทั้งหมด (All_sprites, Enemies, Bullets)

CollisionManager

ตรวจสอบการชนกันและสั่งการเมื่อเกิดการชน (แยก Logic นี้ออกจาก Game Loop)

ResourceManager

โหลดและเก็บภาพ/เสียง (Cache) เพื่อไม่ให้โหลดซ้ำซ้อน (Flyweight Pattern)

การจัดการ Events

Pygame มีระบบ Event Queue ของตัวเอง เราสามารถสร้าง Custom Event ได้

# กำหนด Event ID ใหม่
EVENT_ENEMY_DIED = pygame.USEREVENT + 1

# เมื่อศัตรูตาย (ใน Enemy Class)
pygame.event.post(pygame.event.Event(EVENT_ENEMY_DIED))

# ใน Main Loop (หรือ ScoreManager)
if event.type == EVENT_ENEMY_DIED:
  score += 100

เทคนิคนี้ช่วยลดการผูกมัด (Decoupling) ระหว่าง Enemy และ Score System

การใช้ Abstract Base Class (ABC)

บังคับให้ Game Object ทุกตัวต้องมีโครงสร้างตามสัญญา (Contract)

from abc import ABC, abstractmethod

class Entity(pygame.sprite.Sprite, ABC):
    def __init__(self, groups):
        super().__init__(groups)

    @abstractmethod
    def movement(self):
        pass

    @abstractmethod
    def attack(self):
        pass

Mixins for Reusability

ใช้ Multiple Inheritance เพื่อแปะความสามารถให้ Class (Composition via Inheritance)

AnimatedMixin

มี method animate() สำหรับสลับเฟรมภาพ

ShooterMixin

มี method shoot() และจัดการ Cooldown

class Player(Entity, AnimatedMixin, ShooterMixin):
  pass

สรุปหลักการก่อนลงมือทำ

ใช้ Class แทน Global Variables
ใช้ Sprite Group จัดการ Object จำนวนมาก
แยก Update (Logic) ออกจาก Draw (Render)
ใช้ Inheritance เมื่อเป็นประเภทเดียวกัน (Is-A)
ใช้ Composition เพื่อเพิ่มความสามารถ (Has-A)

เข้าสู่ ห้องปฏิบัติการ

Workshop: สร้างเกม "Space Shooter" ด้วยสถาปัตยกรรม OOP

🚀

Player & Movement

👾

Enemy Factory

🔫

Weapon System

Step 1: โครงสร้างหลัก (The Game Class)

สร้างคลาส Game เพื่อเป็น Manager หลัก (แทนที่จะเขียน loop ลอยๆ)

import pygame, sys

class Game:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((800, 600))
        self.clock = pygame.time.Clock()
        self.running = True
        
        # Groups: หัวใจของการจัดการ Sprite ใน Pygame
        self.all_sprites = pygame.sprite.Group()

    def handle_input(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False

    def update(self):
        self.all_sprites.update() # Polymorphism: เรียก update ของทุกตัว

    def draw(self):
        self.screen.fill((30, 30, 30))
        self.all_sprites.draw(self.screen)
        pygame.display.flip()

    def run(self):
        while self.running:
            self.handle_input()
            self.update()
            self.draw()
            self.clock.tick(60)

Step 2: ผู้เล่น (Player Class)

สืบทอดจาก pygame.sprite.Sprite เพื่อใช้ความสามารถของ Pygame

  • image: พื้นผิวของรูปภาพ (Surface)
  • rect: กรอบสี่เหลี่ยมระบุตำแหน่ง (x, y)
  • update(): เมธอดที่จะถูกเรียกทุกเฟรม
class Player(pygame.sprite.Sprite):
    def __init__(self, groups):
        super().__init__(groups) # Add to groups auto
        self.image = pygame.Surface((50, 40))
        self.image.fill('red')
        self.rect = self.image.get_rect(center=(400, 500))
        self.speed = 5

    def input(self):
        keys = pygame.key.get_pressed()
        if keys[pygame.K_RIGHT]: self.rect.x += self.speed
        if keys[pygame.K_LEFT]: self.rect.x -= self.speed

    def update(self):
        self.input() # SRP: แยกรับค่าอินพุต
        self.constraint() # SRP: แยกการจำกัดขอบเขต

Step 3: อาวุธ (Composition)

แทนที่จะเขียนโค้ดยิงปืนใน Player ให้สร้างคลาส Weapon (Has-A relationship)

class Laser(pygame.sprite.Sprite):
    def __init__(self, pos, groups):
        super().__init__(groups)
        self.image = pygame.Surface((4, 20))
        self.image.fill('yellow')
        self.rect = self.image.get_rect(midbottom=pos)
        self.speed = -10

    def update(self):
        self.rect.y += self.speed
        if self.rect.bottom < 0: self.kill() # ทำลายตัวเองเมื่อออกนอกจอ

# ใน Player Class เพิ่ม:
def shoot(self):
    # Player สร้าง Laser แต่ Laser จัดการการเคลื่อนที่เอง
    Laser(self.rect.midtop, self.groups)

Step 4: ศัตรู (Factory Pattern)

class Enemy(pygame.sprite.Sprite):
    def __init__(self, pos, type, groups):
        super().__init__(groups)
        if type == 'A':
             self.image = ...
             self.speed = 2
        elif type == 'B':
             self.image = ...
             self.speed = 5
        self.rect = self.image.get_rect(topleft=pos)
class EnemyFactory:
    @staticmethod
    def spawn(groups):
        x = random.randint(0, 800)
        y = random.randint(-100, -40)
        type = random.choice(['A', 'B'])
        return Enemy((x,y), type, groups)
Factory ช่วยซ่อนความซับซ้อนของการสุ่มตำแหน่งและประเภทศัตรูออกจาก Game Loop

Step 5: การชน (Collision Manager)

แยกตรรกะการชนออกจากตัว Sprite (Mediator/Manager)

class CollisionManager:
    def __init__(self, player, enemy_group, laser_group):
        self.player = player
        self.enemies = enemy_group
        self.lasers = laser_group

    def check(self):
        # Laser ชน Enemy
        if pygame.sprite.groupcollide(self.lasers, self.enemies, True, True):
            # ส่ง Event หรือเพิ่มคะแนนที่นี่
            print("Enemy Destroyed")

        # Enemy ชน Player
        if pygame.sprite.spritecollide(self.player, self.enemies, True):
            print("Player Hit!")
            self.player.take_damage()

Step 6: คะแนน (Score System)

class ScoreManager:
    _instance = None  # Singleton Pattern (แบบง่าย)

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.score = 0
            cls._instance.font = pygame.font.Font(None, 36)
        return cls._instance

    def add(self, amount):
        self.score += amount

    def draw(self, surface):
        surf = self.font.render(f'Score: {self.score}', True, 'white')
        surface.blit(surf, (10, 10))

Step 7: รวมร่าง (Integration)

กลับไปที่คลาส Game และเชื่อมต่อทุกอย่าง

class Game:
    def __init__(self):
        # ... init pygame ...
        self.all_sprites = pygame.sprite.Group()
        self.enemy_group = pygame.sprite.Group()
        self.laser_group = pygame.sprite.Group()

        # สร้าง Player
        self.player = Player([self.all_sprites])
        self.player.laser_group = self.laser_group # Dependency Injection

        # Managers
        self.collision = CollisionManager(self.player, self.enemy_group, self.laser_group)
        self.score_manager = ScoreManager()

        # ตั้งเวลา Spawn ศัตรู (Event)
        self.ENEMY_SPAWN = pygame.USEREVENT + 1
        pygame.time.set_timer(self.ENEMY_SPAWN, 1000)

    def handle_input(self):
        for event in pygame.event.get():
            if event.type == self.ENEMY_SPAWN:
                enemy = EnemyFactory.spawn([self.all_sprites, self.enemy_group])

    def update(self):
        self.all_sprites.update()
        self.collision.check()

    def draw(self):
        self.screen.fill('black')
        self.all_sprites.draw(self.screen)
        self.score_manager.draw(self.screen)
        pygame.display.flip()

Step 8: การปรับปรุง (Optimization & Clean Code)

Delta Time

เพื่อให้เกมลื่นไหลในทุกเฟรมเรท

dt = self.clock.tick() / 1000
self.rect.x += self.speed * dt

Object Pooling (Advance)

แทนที่จะ kill() และสร้างใหม่ตลอดเวลา ให้นำกระสุนที่ใช้นำกลับมาใช้ใหม่ (Reuse) ช่วยลดภาระ Garbage Collector

ภารกิจท้าทาย (Lab Challenge)

1

เพิ่ม Boss Enemy (OCP)

สร้างคลาส Boss ที่สืบทอดจาก Enemy โดยมี HP และยิงกระสุนสวนกลับได้ ห้ามแก้โค้ด Game Loop

2

เปลี่ยนปืน (Strategy Pattern)

สร้างคลาส WeaponStrategy ให้ Player เปลี่ยนรูปแบบการยิง (เดี่ยว, กระจาย) ได้ขณะเล่น

จบการนำเสนอ

"Design Patterns ไม่ใช่กฎข้อบังคับ แต่เป็นเครื่องมือช่วยแก้ปัญหา"

สรุปวันนี้

  • Pygame Sprite System
  • Game Loop Pattern
  • SOLID (SRP, OCP)
  • Factory & Observer

ทรัพยากรเพิ่มเติม