ลุย Day 5 กันต่อเลยครับ!
Dynamic Scene Instantiation & Memory
วันนี้เราจะมาทำความเข้าใจหัวใจของการสร้างเกมแอ็กชัน: การเสกวัตถุขึ้นมาระหว่างเล่น (Runtime Spawning) เช่น กระสุนปืน, ไอเทมดรอป และมอนสเตอร์ ด้วย PackedScene, การเลือกใช้ preload() vs load() พร้อมทั้งบริหารจัดการหน่วยความจำด้วย queue_free() ป้องกัน Memory Leak!
1. Micro-Concept
PackedScene & I/O
2. Turret Drill
สร้างป้อมปืน & กระสุน
3. Memory Check
queue_free() และ Global Pos
คีย์เวิร์ดสำคัญประจำวันนี้
"พิมพ์เขียวสู่หน่วยความจำ: เสกด้วย PackedScene พิกัดด้วย global_position และคืนแรมด้วย queue_free"
1. Micro-Concept Breakdown: 3 กุญแจสำคัญของการเสกวัตถุ (Spawning)
1) PackedScene คืออะไร?
PackedScene เปรียบเสมือน "พิมพ์เขียว (Blueprint)" ที่เก็บโครงสร้างของไฟล์ .tscn เอาไว้
เมื่อเราต้องการสร้างวัตถุจริง เราจะสั่ง .instantiate() เพื่อปั๊มตัวอินสแตนซ์ออกมาสู่หน่วยความจำ (RAM)
2) preload() vs load()
- •
preload("path"): โหลดตั้งแต่ช่วง Compile/Parse สคริปต์ เร็วมาก ไม่กระตุกระหว่างเล่น (No Lag Spike) แต่ต้องใช้ Constant Path - •
load("path"): โหลดจาก Disk ระหว่างรันเกม เปลี่ยนพาทด้วยตัวแปร String ได้ แต่ ห้ามเรียกซ้ำๆ ใน Loop ยิง เพราะจะสะดุด
3) add_child() & queue_free()
• add_child(): นำวัตถุที่อยู่ใน RAM เข้าสู่ SceneTree เพื่อเริ่มรับรอบการประมวลผลและการแสดงผล
• queue_free(): นำโหนดออกจากทรีและลบออกจาก RAM อย่างปลอดภัยเมื่อสิ้นสุดเฟรม (Safe Garbage Cleanup)
เมื่อสร้างกระสุนออกมา อย่าสั่ง add_child(bullet) ใส่ตัวปืนหรือตัวละครตรงๆ เพราะเวลากระบอกปืนหมุน กระสุนที่กำลังบินอยู่จะ หมุนและแกว่งตามปืนไปด้วย! ทางที่ถูกต้องคือ กำหนดพิกัด bullet.global_position ให้ตรงกับปากกระบอก แล้วนำกระสุนไปแปะไว้ที่ซีนหลัก (get_tree().current_scene.add_child(bullet)) เพื่อให้กระสุนบินอย่างอิสระครับ
2. Hands-on Drill: สร้างระบบยิงกระสุน (TurretMuzzle & Bullet)
เราจะสร้าง 2 ซีนแยกออกจากกันอย่างเป็นระเบียบ: ซีนกระสุน (Bullet.tscn) ที่พุ่งไปข้างหน้าและทำลายตัวเองอัตโนมัติ และซีนป้อมปืน (Turret.tscn) ที่หมุนตามเมาส์และยิงกระสุนออกไป
├── Sprite2D [รูปกระสุน]
└── CollisionShape2D [ขอบเขตการชน]
└── Marker2D (Muzzle) [แนบสคริปต์ TurretMuzzle.gd]
class_name Bullet
extends Area2D
## Bullet: สคริปต์ควบคุมการเคลื่อนที่และการทำลายตัวเองของกระสุน
@export var speed: float = 600.0
@export var lifetime: float = 3.0
func _ready() -> void:
# สร้าง Timer เพื่อนับเวลาทำลายตัวเอง ป้องกัน Memory Leak จากกระสุนลอยออกนอกจอไม่สิ้นสุด
var cleanup_timer := Timer.new()
add_child(cleanup_timer)
cleanup_timer.wait_time = lifetime
cleanup_timer.one_shot = true
cleanup_timer.timeout.connect(queue_free)
cleanup_timer.start()
func _process(delta: float) -> void:
# พุ่งไปข้างหน้าตามทิศทางที่ปืนหมุนไป (transform.x คือทิศทาง Vector2.RIGHT ของ Node)
position += transform.x * speed * delta
class_name TurretMuzzle
extends Marker2D
## TurretMuzzle: ตัวจัดการสร้างกระสุนแบบ Dynamic Instantiation
# เปิดช่องให้เลือกไฟล์ Bullet.tscn ในหน้า Inspector
@export var bullet_scene: PackedScene
@export var fire_rate: float = 0.2
var can_fire: bool = true
@onready var cooldown_timer: Timer = Timer.new()
func _ready() -> void:
# ติดตั้ง Timer สำหรับจัดการคูลดาวน์การยิง
add_child(cooldown_timer)
cooldown_timer.wait_time = fire_rate
cooldown_timer.one_shot = true
cooldown_timer.timeout.connect(func(): can_fire = true)
func _process(_delta: float) -> void:
# ให้ป้อมปืนหมุนมองตามตำแหน่งเม้าส์
var parent_node := get_parent() as Node2D
if parent_node:
parent_node.look_at(get_global_mouse_position())
# ยิงกระสุนเมื่อคลิกเม้าส์ซ้าย หรือกดปุ่ม Spacebar/Enter
if Input.is_action_pressed("ui_accept") or Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
fire_bullet()
func fire_bullet() -> void:
if not can_fire:
return
if not bullet_scene:
push_error("ยังไม่ได้ใส่ Bullet Scene ใน Inspector!")
return
# 1. Instantiate กระสุนปืนจาก PackedScene เข้าสู่หน่วยความจำ
var bullet_instance := bullet_scene.instantiate() as Node2D
# 2. กำหนดตำแหน่งและองศาหมุนให้เท่ากับปากกระบอกปืน (Marker2D)
bullet_instance.global_position = global_position
bullet_instance.global_rotation = global_rotation
# 3. นำกระสุนใส่เข้าไปใน Scene หลัก (Current Scene) เพื่อไม่ให้ตำแหน่งกระสุนผูกติดกับการหมุนของปืน
get_tree().current_scene.add_child(bullet_instance)
# 4. เริ่มคูลดาวน์
can_fire = false
cooldown_timer.start()
3. ขั้นตอนการลงมือทำจริงใน Godot Engine 4
สเต็ปประกอบชิ้นส่วนใน Editor:
- สร้าง Scene กระสุน (Bullet.tscn):
- สร้าง Scene ใหม่ เลือก Root Node เป็น
Area2Dตั้งชื่อว่าBullet - เพิ่มลูก
Sprite2D(ใส่ Texture ไอคอนหรือรูปทรงกลมเล็กๆ) และCollisionShape2D(ใส่ CircleShape2D) - แนบสคริปต์
Bullet.gdและกด Save Scene เป็นres://Bullet.tscn
- สร้าง Scene ใหม่ เลือก Root Node เป็น
- สร้าง Scene ป้อมปืน (Turret.tscn):
- สร้าง Scene ใหม่ เลือก Root Node เป็น
Node2Dตั้งชื่อว่าTurret(อาจใส่ Sprite2D ลำกล้องปืนเพื่อให้เห็นทิศทางชัดเจน) - เพิ่มลูกเป็น
Marker2Dตั้งชื่อว่าMuzzleแล้วลากขยับไปทางขวามือเล็กน้อย (ทิศแกน X เป็นทิศหน้าของ 2D ใน Godot) - แนบสคริปต์
TurretMuzzle.gdไว้ที่โหนดMuzzle
- สร้าง Scene ใหม่ เลือก Root Node เป็น
- กำหนดค่าใน Inspector:
คลิกที่โหนด
Muzzleดูหน้าต่าง Inspector ด้านขวา จะพบช่อง Bullet Scene ให้ลากไฟล์Bullet.tscnจากช่อง FileSystem มาวางใส่ช่องนี้ - ทดสอบการรัน:
กด Play Scene (F6) ที่ซีนป้อมปืน ลองเลื่อนเมาส์ไปมา ป้อมปืนจะหมุนเล็งตามเมาส์ และเมื่อคลิกเมาส์ซ้าย กระสุนจะพุ่งออกจากปากกระบอกปืนอย่างแม่นยำ และทำลายตัวเองอัตโนมัติเมื่อครบ 3 วินาที!
ในระหว่างที่เกมกำลังรันอยู่ ให้สลับไปดูที่แถบ Debugger > Monitors > Objects ใน Godot Editor ด้านล่าง ลองกดยิงกระสุนรัวๆ สังเกตว่าจำนวนโหนดจะเพิ่มขึ้น และหลังจาก 3 วินาทีจำนวนโหนดจะลดลงกลับมาเท่าเดิมเสมอ นั่นแปลว่าระบบ queue_free() ทำงานได้สมบูรณ์และไม่มีหน่วยความจำรั่วไหลครับ
4. 🎯 Mini-Check สำหรับ Day 5
ลองทดสอบความเข้าใจ 2 ข้อสั้นๆ เกี่ยวกับการบริหารหน่วยความจำและ Scene Tree ครับ:
fire_bullet() เราถึงสั่ง get_tree().current_scene.add_child(bullet_instance) แทนที่จะสั่ง add_child(bullet_instance) ตรงๆ ใส่ปากกระบอกปืน (Marker2D)?
กดดูเฉลยข้อ 1
หากนำกระสุนไปเป็นลูกของ Muzzle หรือป้อมปืน เมื่อผู้เล่นขยับเมาส์หมุนปืนไปทางอื่น กระสุนทุกลูกที่กำลังบินลอยอยู่กลางอากาศจะ หมุนและเหวี่ยงตามการเล็งของปืนไปด้วย ซึ่งผิดธรรมชาติของกระสุนปืนอย่างมาก การส่งกระสุนไปไว้ที่ Root Scene ของโลกเกม (current_scene) ทำให้กระสุนมีอิสระในการเคลื่อนที่ตามทิศทางของตัวเองโดยสิ้นเชิงครับ
bullet_scene.instantiate() แต่ ลืม สั่ง add_child() จะเกิดอะไรขึ้นกับกระสุนปืนลูกนั้น?
กดดูเฉลยข้อ 2
เพราะคำสั่ง .instantiate() ทำหน้าที่เพียงแค่จัดสรรพื้นที่ในหน่วยความจำ RAM เท่านั้น หากไม่ได้ถูก add_child() เข้าสู่ SceneTree:
1. ตัวกระสุนจะไม่ถูกแสดงผลบนจอภาพ
2. วงจรชีวิตอย่าง _ready() และ _process() จะไม่ถูกเรียกทำงานเลยแม้แต่เฟรมเดียว
3. วัตถุนี้จะไม่ถูก Garbage Collector เก็บกวาด และจะกิน RAM สะสมไปเรื่อยๆ จนเกมแครช
พร้อมต่อ Day 6 แล้วหรือยังครับ?
วันพรุ่งนี้เราจะเข้าสู่ธีมน้ำเงินเข้มโคบอลต์ Day 6: Pure Component Design (HealthComponent) สร้างคอมโพเนนต์บริหารเลือดและพลังชีวิตแบบอิสระ 100% พร้อมระบบ Signals "Call Down, Signal Up"!