Level 1 • Day 6 Hands-on

Pure Component Design (HealthComponent)

Day 6 of 7-Day Foundation

ยินดีต้อนรับสู่ Day 6 ครับ!
Pure Component Design & Health System

วันนี้เราจะมาเรียนรู้สถาปัตยกรรม Pure Component Design ซึ่งเป็นหนึ่งในแนวคิดหลักของการเขียนโค้ด Godot 4 ให้เป็นแบบ Modular โดยเราจะสร้าง HealthComponent ที่สามารถนำไปเสียบยึดกับ Entity ตัวไหนในเกมก็ได้ ไม่ว่าจะเป็น ผู้เล่น (Player), ศัตรู (Enemy), หรือสิ่งของที่ทำลายได้ (Breakable Crate)!

8m

1. Micro-Concept

Pure Leaf & Signal Up

15m

2. Health Drill

สร้าง HealthComponent

5m

3. Integration Test

ทดสอบกับ BreakableCrate

D6

คีย์เวิร์ดสำคัญประจำวันนี้

"คอมโพเนนต์ที่ดีต้องไร้เงาของโหนดแม่: รับคำสั่งลงมา (Call Down) ส่งสัญญาณขึ้นไป (Signal Up)"

Single Responsibility Principle
Decoupled 100% ไร้ Hardcoded Parent
clampi() ป้องกัน HP ทะลุขอบเขต
Step 1 • 5–10 Minutes Breakdown

1. Micro-Concept Breakdown: 3 เสาหลักของการออกแบบ Pure Component

Decoupled Architecture

1) Pure Component คืออะไร?

• Single Responsibility: จัดการเรื่องใดเรื่องหนึ่งอย่างโดดเดี่ยว เช่น HealthComponent มีหน้าที่นับ HP, หักลบ Damage และฟื้นฟู Heal เท่านั้น

• Decoupled: ไม่ต้องรู้ว่าแม่คือ Player, Enemy หรือ BreakableCrate ขอแค่ถูกสั่งผ่านฟังก์ชันก็พอ

Reusable 100% แปะใช้ซ้ำได้กับทุกวัตถุ

2) สถาปัตยกรรม Signal-Driven

ตามหลัก "Call Down, Signal Up":

เมื่อเลือดลด HealthComponent จะ ไม่ วิ่งไปแก้ข้อความบน UI Label หรือสั่งเล่นเสียง Effect เองโดยตรง

แต่จะตะโกนผ่าน Signal (health_changed, died) ให้ระบบภายนอกมารับไปจัดการเอง

ตัดการเชื่อมโยงข้ามซีนอย่างสมบูรณ์

3) Safe Clamping ด้วย clampi()

ใน GDScript มีฟังก์ชันในตัวคือ clampi(value, min, max)

ช่วยจำกัดค่าตัวเลขให้อยู่ในช่วงที่กำหนดอย่างปลอดภัย:
• ไม่ให้ HP ติดลบต่ำกว่า 0
• ไม่ให้การฮีลล้นเกิน max_health

หมดปัญหาบัคเลือดติดลบหรือล้นหลอด
กฎเหล็กการออกแบบ Leaf Component (The Leaf Rule):

คอมโพเนนต์ที่เป็น "ใบไม้ (Leaf Node)" ต้องไม่มีการเรียก get_parent() เพื่อเรียกใช้ฟังก์ชันเฉพาะทางของแม่ และต้องไม่ใช้ get_tree().root ข้ามไปสั่งระบบ UI เด็ดขาด ข้อมูลเข้าต้องมาทางพารามิเตอร์ของฟังก์ชัน และข้อมูลออกต้องส่งผ่าน Signal เท่านั้นครับ!

Step 2 • 15–20 Mins Drill

2. Hands-on Drill: สร้าง HealthComponent.gd

เราจะสร้าง Component สำหรับจัดการค่าพลังชีวิตและความเสียหาย ที่แยกตัวเป็นอิสระ มีระบบแจ้งเตือนผ่าน Signal และรองรับฟังก์ชันเพิ่มเลือด (Heal) อย่างครบวงจร

โครงสร้าง Node Tree ใน Godot Editor
Node2D (BreakableCrate / Player / Enemy)
├── Sprite2D [รูปภาพของ Entity]
└── HealthComponent (Node) [แนบสคริปต์ HealthComponent.gd]

HealthComponent สืบทอดจาก Node ธรรมดา ไม่ต้องมี Transform 2D/3D ให้กินหน่วยความจำโดยไม่จำเป็น

ไฟล์: HealthComponent.gd Signals + Clamped Math + Strict Typing
class_name HealthComponent
extends Node

## HealthComponent: Component บริสุทธิ์สำหรับจัดการพลังชีวิตและความเสียหาย

# Signal สำหรับแจ้งเตือนเมื่อ HP เปลี่ยนแปลง หรือเมื่อตัวละครตาย
signal health_changed(current_health: int, max_health: int)
signal died

@export var max_health: int = 100
var current_health: int

func _ready() -> void:
    current_health = max_health
    # ส่ง Signal แจ้งค่าเริ่มต้นให้ระบบที่รอฟัง (เช่น HUD) ทราบ
    health_changed.emit(current_health, max_health)

## ฟังก์ชันรับความเสียหาย
func take_damage(amount: int) -> void:
    if current_health <= 0:
        return

    # ล็อกค่าให้อยู่ในช่วง 0 ถึง max_health
    current_health = clampi(current_health - amount, 0, max_health)
    health_changed.emit(current_health, max_health)

    print_rich("[color=orange][HealthComponent] HP เหลือ: %d/%d[/color]" % [current_health, max_health])

    if current_health == 0:
        died.emit()

## ฟังก์ชันฟื้นฟูพลังชีวิต
func heal(amount: int) -> void:
    if current_health <= 0 or current_health >= max_health:
        return

    current_health = clampi(current_health + amount, 0, max_health)
    health_changed.emit(current_health, max_health)
    print_rich("[color=green][HealthComponent] Heal! HP เพิ่มเป็น: %d/%d[/color]" % [current_health, max_health])
Step 3 • Hands-on Workflow & Testing

3. ขั้นตอนการลงมือทำและการทดสอบจริงใน Godot Engine 4

สเต็ปสร้างและทดสอบใน Editor:

  1. สร้าง Component:
    • สร้าง Scene ใหม่ ชนิด Root Node เป็น Node ตั้งชื่อว่า HealthComponent
    • แนบสคริปต์ HealthComponent.gd และบันทึกไฟล์เป็น res://HealthComponent.gd
  2. สร้าง Entity ทดสอบ (เช่น กล่องไม้ทำลายได้):
    • สร้าง 2D Scene ใหม่ ใช้ Root Node เป็น Node2D ตั้งชื่อว่า BreakableCrate
    • เพิ่มลูก Sprite2D แล้วใส่รูปกล่อง (หรือรูปไอคอน Godot)
    • เพิ่มโหนด HealthComponent เป็นลูกของ BreakableCrate
  3. เชื่อมต่อ Signal สั่งลบกล่องเมื่อตาย:

    เลือกโหนด HealthComponent ดูที่แท็บ Node > Signals ด้านขวา ดับเบิ้ลคลิกที่ Signal died() เพื่อเชื่อมต่อเข้ากับสคริปต์ของ BreakableCrate.gd

  4. แนบสคริปต์และทดสอบการรับความเสียหาย:

    แนบสคริปต์ให้กับ BreakableCrate โดยเขียนฟังก์ชันรับความเสียหายและลบตนเองเมื่อสิ้นชีพ:

สคริปต์ทดสอบ: BreakableCrate.gd (แนบกับ Node2D) Parent Listening to Child Signal
extends Node2D

@onready var health_component: HealthComponent = $HealthComponent

func _ready() -> void:
    # เชื่อมต่อ Signal died แบบโค้ด หรือเชื่อมผ่าน Editor ก็ได้
    health_component.died.connect(_on_health_component_died)

## Callback เมื่อ HP กล่องหมดเป็น 0
func _on_health_component_died() -> void:
    print_rich("[color=red]กล่องถูกทำลายเรียบร้อยแล้ว![/color]")
    queue_free() # ทำลายกล่องทิ้งเมื่อ HP หมด

## ทดสอบกด Spacebar หรือคลิกซ้ายเพื่อลด HP ทีละ 25
func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("ui_accept"):
        health_component.take_damage(25)
ผลการทดสอบการรัน (Play Scene - F6):

กด Play Scene (F6) แล้วลองกดปุ่ม Spacebar สังเกตใน Console Output จะเห็นข้อความสีส้มแจ้งเตือนว่า HP เหลือ: 75/100, 50/100, 25/100 และเมื่อกดครั้งที่สี่ HP จะเหลือ 0 สัญญาณ died จะถูกปล่อยออกมา ส่งผลให้กล่องพิมพ์ข้อความสีแดงและเรียก queue_free() สลายตัวไปทันที!

Reflect & Validate

4. 🎯 Mini-Check สำหรับ Day 6

ลองทดสอบความเข้าใจ 2 ข้อสั้นๆ เกี่ยวกับสถาปัตยกรรม Component-Based ครับ:

ข้อ 1: ทำไม HealthComponent ถึง ไม่ควร เขียนโค้ดสั่งให้เปลี่ยนฉาก หรือสั่งให้ UI อัปเดตหลอดเลือดโดยตรงในสคริปต์ของมันเอง?
กดดูเฉลยข้อ 1
คำตอบ: เพื่อรักษาความเป็นอิสระ (Decoupling) และนำกลับมาใช้ซ้ำได้ (Reusability)!

หาก HealthComponent ไปสั่งงาน UI หรือสั่งเปลี่ยนฉาก มันจะไม่สามารถนำไปใช้กับ Entity อื่นๆ เช่น มอนสเตอร์ หรือ กล่องไม้ได้อีก เพราะมอนสเตอร์ไม่มีหลอดเลือด UI ของผู้เล่น และกล่องไม้ตายแล้วไม่ต้องเปลี่ยนฉากเกม! การส่ง Signal ออกไปแทนทำให้คอมโพเนนต์นี้บริสุทธิ์ 100% ใครอยากฟังไปทำอะไรต่อก็เชื่อมต่อ Signal ไปตามบริบทของตนเองครับ

ข้อ 2: หากเรานำ HealthComponent นี้ไปใส่ให้กับมอนสเตอร์ (Enemy) เราต้องเขียนสคริปต์คำนวณ HP เพิ่มเติมในตัวมอนสเตอร์อีกหรือไม่?
กดดูเฉลยข้อ 2
คำตอบ: ไม่ต้องเขียนซ้ำแม้แต่บรรทัดเดียว!

นี่คือพลังสูงสุดของ Composition Over Inheritance ตัวมอนสเตอร์มีหน้าที่แค่เพิ่ม HealthComponent เป็นลูก ปรับค่า max_health ใน Inspector ตามความอึดของมอนสเตอร์ตัวนั้น และเชื่อม Signal died เข้ากับฟังก์ชันแอนิเมชันตายหรือดรอปไอเทมของมอนสเตอร์ โดยไม่ต้องเขียนตัวแปร HP หรือฟังก์ชันลดเลือดซ้ำซ้อนอีกต่อไปครับ

Graduation Day Ahead

พร้อมสำหรับวันจบการศึกษา Day 7 แล้วหรือยังครับ?

วันพรุ่งนี้เราจะเข้าสู่ธีมชมพูมาเจนต้า Day 7: Weekly Review Quiz & Graduation Exam รวบยอดความรู้ 6 วันที่ผ่านมา สร้างมินิเกมวัดผลจริง "Modular Cannon Defense" เพื่อรับเกรดสถาปัตยกรรมระดับ Architect Grade!

คัดลอก GDScript ไปยังคลิปบอร์ดแล้ว!