ยินดีต้อนรับสู่ Day 6 ครับ!
Pure Component Design & Health System
วันนี้เราจะมาเรียนรู้สถาปัตยกรรม Pure Component Design ซึ่งเป็นหนึ่งในแนวคิดหลักของการเขียนโค้ด Godot 4 ให้เป็นแบบ Modular โดยเราจะสร้าง HealthComponent ที่สามารถนำไปเสียบยึดกับ Entity ตัวไหนในเกมก็ได้ ไม่ว่าจะเป็น ผู้เล่น (Player), ศัตรู (Enemy), หรือสิ่งของที่ทำลายได้ (Breakable Crate)!
1. Micro-Concept
Pure Leaf & Signal Up
2. Health Drill
สร้าง HealthComponent
3. Integration Test
ทดสอบกับ BreakableCrate
คีย์เวิร์ดสำคัญประจำวันนี้
"คอมโพเนนต์ที่ดีต้องไร้เงาของโหนดแม่: รับคำสั่งลงมา (Call Down) ส่งสัญญาณขึ้นไป (Signal Up)"
1. Micro-Concept Breakdown: 3 เสาหลักของการออกแบบ Pure Component
1) Pure Component คืออะไร?
• Single Responsibility: จัดการเรื่องใดเรื่องหนึ่งอย่างโดดเดี่ยว เช่น HealthComponent มีหน้าที่นับ HP, หักลบ Damage และฟื้นฟู Heal เท่านั้น
• Decoupled: ไม่ต้องรู้ว่าแม่คือ Player, Enemy หรือ BreakableCrate ขอแค่ถูกสั่งผ่านฟังก์ชันก็พอ
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 Node)" ต้องไม่มีการเรียก get_parent() เพื่อเรียกใช้ฟังก์ชันเฉพาะทางของแม่ และต้องไม่ใช้ get_tree().root ข้ามไปสั่งระบบ UI เด็ดขาด ข้อมูลเข้าต้องมาทางพารามิเตอร์ของฟังก์ชัน และข้อมูลออกต้องส่งผ่าน Signal เท่านั้นครับ!
2. Hands-on Drill: สร้าง HealthComponent.gd
เราจะสร้าง Component สำหรับจัดการค่าพลังชีวิตและความเสียหาย ที่แยกตัวเป็นอิสระ มีระบบแจ้งเตือนผ่าน Signal และรองรับฟังก์ชันเพิ่มเลือด (Heal) อย่างครบวงจร
├── Sprite2D [รูปภาพของ Entity]
└── HealthComponent (Node) [แนบสคริปต์ HealthComponent.gd]
HealthComponent สืบทอดจาก Node ธรรมดา ไม่ต้องมี Transform 2D/3D ให้กินหน่วยความจำโดยไม่จำเป็น
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])
3. ขั้นตอนการลงมือทำและการทดสอบจริงใน Godot Engine 4
สเต็ปสร้างและทดสอบใน Editor:
- สร้าง Component:
- สร้าง Scene ใหม่ ชนิด Root Node เป็น
Nodeตั้งชื่อว่าHealthComponent - แนบสคริปต์
HealthComponent.gdและบันทึกไฟล์เป็นres://HealthComponent.gd
- สร้าง Scene ใหม่ ชนิด Root Node เป็น
- สร้าง Entity ทดสอบ (เช่น กล่องไม้ทำลายได้):
- สร้าง 2D Scene ใหม่ ใช้ Root Node เป็น
Node2Dตั้งชื่อว่าBreakableCrate - เพิ่มลูก
Sprite2Dแล้วใส่รูปกล่อง (หรือรูปไอคอน Godot) - เพิ่มโหนด
HealthComponentเป็นลูกของBreakableCrate
- สร้าง 2D Scene ใหม่ ใช้ Root Node เป็น
- เชื่อมต่อ Signal สั่งลบกล่องเมื่อตาย:
เลือกโหนด
HealthComponentดูที่แท็บ Node > Signals ด้านขวา ดับเบิ้ลคลิกที่ Signaldied()เพื่อเชื่อมต่อเข้ากับสคริปต์ของBreakableCrate.gd - แนบสคริปต์และทดสอบการรับความเสียหาย:
แนบสคริปต์ให้กับ
BreakableCrateโดยเขียนฟังก์ชันรับความเสียหายและลบตนเองเมื่อสิ้นชีพ:
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) แล้วลองกดปุ่ม Spacebar สังเกตใน Console Output จะเห็นข้อความสีส้มแจ้งเตือนว่า HP เหลือ: 75/100, 50/100, 25/100 และเมื่อกดครั้งที่สี่ HP จะเหลือ 0 สัญญาณ died จะถูกปล่อยออกมา ส่งผลให้กล่องพิมพ์ข้อความสีแดงและเรียก queue_free() สลายตัวไปทันที!
4. 🎯 Mini-Check สำหรับ Day 6
ลองทดสอบความเข้าใจ 2 ข้อสั้นๆ เกี่ยวกับสถาปัตยกรรม Component-Based ครับ:
HealthComponent ถึง ไม่ควร เขียนโค้ดสั่งให้เปลี่ยนฉาก หรือสั่งให้ UI อัปเดตหลอดเลือดโดยตรงในสคริปต์ของมันเอง?
กดดูเฉลยข้อ 1
หาก HealthComponent ไปสั่งงาน UI หรือสั่งเปลี่ยนฉาก มันจะไม่สามารถนำไปใช้กับ Entity อื่นๆ เช่น มอนสเตอร์ หรือ กล่องไม้ได้อีก เพราะมอนสเตอร์ไม่มีหลอดเลือด UI ของผู้เล่น และกล่องไม้ตายแล้วไม่ต้องเปลี่ยนฉากเกม! การส่ง Signal ออกไปแทนทำให้คอมโพเนนต์นี้บริสุทธิ์ 100% ใครอยากฟังไปทำอะไรต่อก็เชื่อมต่อ Signal ไปตามบริบทของตนเองครับ
HealthComponent นี้ไปใส่ให้กับมอนสเตอร์ (Enemy) เราต้องเขียนสคริปต์คำนวณ HP เพิ่มเติมในตัวมอนสเตอร์อีกหรือไม่?
กดดูเฉลยข้อ 2
นี่คือพลังสูงสุดของ Composition Over Inheritance ตัวมอนสเตอร์มีหน้าที่แค่เพิ่ม HealthComponent เป็นลูก ปรับค่า max_health ใน Inspector ตามความอึดของมอนสเตอร์ตัวนั้น และเชื่อม Signal died เข้ากับฟังก์ชันแอนิเมชันตายหรือดรอปไอเทมของมอนสเตอร์ โดยไม่ต้องเขียนตัวแปร HP หรือฟังก์ชันลดเลือดซ้ำซ้อนอีกต่อไปครับ
พร้อมสำหรับวันจบการศึกษา Day 7 แล้วหรือยังครับ?
วันพรุ่งนี้เราจะเข้าสู่ธีมชมพูมาเจนต้า Day 7: Weekly Review Quiz & Graduation Exam รวบยอดความรู้ 6 วันที่ผ่านมา สร้างมินิเกมวัดผลจริง "Modular Cannon Defense" เพื่อรับเกรดสถาปัตยกรรมระดับ Architect Grade!