Level 1 • Day 3 Hands-on

Node Lifecycles & Execution Order

Day 3 of 7-Day Foundation

ลุย Day 3 กันต่อเลยครับ!
Node Lifecycles & Execution Order

วันนี้เราจะมาทำความเข้าใจ วงจรชีวิตและลำดับการทำงาน (Lifecycle) ของ Node ใน Godot 4 เพื่อไขข้อข้องใจว่าทำไมการอ้างอิงโหนดลูกใน _init() จึงพัง และเหตุใด _ready() ถึงทำงานแบบ Bottom-Up จากลูกขึ้นหาแม่เสมอ!

8m

1. Micro-Concept

5 ลำดับขั้นของ Lifecycle

15m

2. Tracker Drill

Parent & Child Code Verification

5m

3. Console Test

สังเกต Log ลำดับการรันจริง

D3

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

"จำลำดับให้ขึ้นใจ: สร้างก่อน เข้าทรีทีหลัง ลูกเรดี้ก่อน แม่จึงพร้อม"

ห้ามอ้างอิงโหนดลูกใน _init()
_ready() ทำงานแบบ Bottom-Up เสมอ
ฟิสิกส์ & การชนใช้ _physics_process()
Step 1 • 5–10 Minutes Breakdown

1. Micro-Concept: 5 ลำดับขั้นของวงจรชีวิต Node ใน Godot 4

Engine Execution Chain
Step 1

_init()

ถูกเรียกตอนสร้างวัตถุใน RAM (Constructor) ยังไม่มีโหนดลูก และยังไม่อยู่ใน SceneTree

ห้ามเรียก $ChildNode
Step 2

_enter_tree()

โหนดเริ่มถูกนำเข้าสู่ SceneTree แต่ลูกๆ อาจยังไม่พร้อม มักใช้ลงทะเบียน Singleton

เชื่อมโยง Global Hub
Step 3 • Bottom-Up

_ready()

โหนดลูกรันเสร็จก่อนแม่เสมอ! เป็นจุดที่ปลอดภัย 100% ในการเข้าถึงโหนดลูก

ปลอดภัยต่อการแคช Node
Step 4 • Render Loop

_process(delta)

รันทุก Frame การแสดงผล (เวลาผันผวนตาม FPS) เหมาะกับ UI, กล้อง และ Visual Effects

งานกราฟิก/UI
Step 5 • Fixed 60Hz

_physics_process(delta)

รันตามรอบเวลาคงที่ (Fixed Tick Rate 60 Hz) เหมาะกับคำนวณการชนและการเคลื่อนที่

Character / RigidBody
จุดสำคัญที่ควรรู้เกี่ยวกับ "Bottom-Up Ready":

ลำดับการเข้าสู่ Tree (_enter_tree()) จะทำงานจาก แม่ลงไปหาลูก (Top-Down) แต่ลำดับความพร้อมสมบูรณ์ (_ready()) จะทำงานแบบ ลูกย้อนกลับขึ้นหาแม่ (Bottom-Up) เพื่อให้มั่นใจว่าเมื่อแม่เริ่มทำงาน โหนดลูกที่เป็นส่วนประกอบภายในจะถูกเตรียมพร้อมและมีข้อมูลครบถ้วนแล้วเสมอ

Step 2 • 15–20 Mins Drill

2. Hands-on Drill: พิสูจน์ลำดับจริงด้วย LifecycleTracker

เราจะสร้าง 2 สคริปต์เพื่อพิมพ์ลำดับการทำงานของ Node แม่ (Parent) และ Node ลูก (Child) ออกทาง Console Output เพื่อดูด้วยตาตนเองว่า _ready() ของลูกจะถูกพิมพ์ก่อนแม่จริงหรือไม่!

โครงสร้าง Node Tree ใน Godot Editor
Node2D (MainParent) [แนบสคริปต์ ParentTracker.gd]
└── Node (ChildNode) [แนบสคริปต์ ChildTracker.gd]
สคริปต์ที่ 1: ParentTracker.gd (แนบกับ MainParent) Parent Lifecycle Sequence
class_name ParentTracker
extends Node2D

func _init() -> void:
    print("1. [PARENT] _init(): สร้างวัตถุ Parent ในหน่วยความจำ")

func _enter_tree() -> void:
    print("2. [PARENT] _enter_tree(): Parent เข้าสู่ SceneTree")

func _ready() -> void:
    # สังเกตว่าบรรทัดนี้จะถูกเรียก *หลังจาก* Child _ready() ทำงานเสร็จแล้วเสมอ
    print("4. [PARENT] _ready(): Parent และลูกๆ สมบูรณ์พร้อมใช้งาน")

func _process(delta: float) -> void:
    print("5. [PARENT] _process(): ประมวลผล Frame (delta: %f)" % delta)
    set_process(false) # ปิดการรัน _process เพื่อไม่ให้พิมพ์รัวจนล้น Console

func _physics_process(delta: float) -> void:
    print("6. [PARENT] _physics_process(): ประมวลผล Physics Tick (delta: %f)" % delta)
    set_physics_process(false) # ปิดการรัน _physics_process หลังทำงานครั้งแรก
สคริปต์ที่ 2: ChildTracker.gd (แนบกับ ChildNode) Child Bottom-Up Ready
class_name ChildTracker
extends Node

func _init() -> void:
    print("   ↳ [CHILD] _init(): สร้างวัตถุ Child ในหน่วยความจำ")

func _enter_tree() -> void:
    print("   ↳ [CHILD] _enter_tree(): Child เข้าสู่ SceneTree")

func _ready() -> void:
    # ลูกจะพร้อมและรัน _ready() เสร็จสมบูรณ์ ก่อนโหนดแม่เสมอ!
    print("3. [CHILD] _ready(): Child ทำงาน ready เสร็จสิ้นก่อน Parent!")
Step 3 • Hands-on Workflow & Testing

3. ขั้นตอนการทดสอบจริงใน Godot Engine 4

สเต็ปสร้าง Scene และสังเกต Output Console:

  1. สร้าง Scene ใหม่: เลือก Root Node เป็น Node2D แล้วตั้งชื่อว่า MainParent
  2. เพิ่ม Child Node: คลิกขวาที่ MainParent เลือก Add Child Node > เลือกประเภท Node แล้วตั้งชื่อว่า ChildNode
  3. แนบสคริปต์ทั้งสอง:
    • แนบ ParentTracker.gd ให้กับโหนด MainParent
    • แนบ ChildTracker.gd ให้กับโหนด ChildNode
  4. ทดสอบการรัน: กด Play Scene (F6) แล้วสังเกตลำดับข้อความในแท็บ Output ด้านล่างของ Godot Editor
ลำดับผลลัพธ์ที่จะปรากฏใน Console Output ของ Godot:
1. [PARENT] _init(): สร้างวัตถุ Parent ในหน่วยความจำ
↳ [CHILD] _init(): สร้างวัตถุ Child ในหน่วยความจำ
2. [PARENT] _enter_tree(): Parent เข้าสู่ SceneTree
↳ [CHILD] _enter_tree(): Child เข้าสู่ SceneTree
3. [CHILD] _ready(): Child ทำงาน ready เสร็จสิ้นก่อน Parent!
4. [PARENT] _ready(): Parent และลูกๆ สมบูรณ์พร้อมใช้งาน
5. [PARENT] _process(): ประมวลผล Frame (delta: 0.016667)
6. [PARENT] _physics_process(): ประมวลผล Physics Tick (delta: 0.016667)
บทสรุปเชิงสถาปัตยกรรม: จะเห็นได้ชัดเจนว่า [CHILD] _ready() ทำงานเสร็จในลำดับที่ 3 ก่อนที่ [PARENT] _ready() จะทำงานในลำดับที่ 4 เสมอ! นี่คือเหตุผลที่โหนดแม่สามารถเข้าถึงตัวแปรของโหนดลูกใน _ready() ได้อย่างปลอดภัย 100%
Reflect & Validate

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

ลองทดสอบความเข้าใจ 2 ข้อสั้นๆ เกี่ยวกับสถานการณ์การใช้งานจริงครับ:

ข้อ 1: โค้ดคำนวณการเคลื่อนที่ของตัวละคร (move_and_slide()) ควรเขียนใน _process หรือ _physics_process?
กดดูเฉลยข้อ 1
คำตอบ: ต้องเขียนใน _physics_process เสมอ!

เพราะการคำนวณการเคลื่อนที่และการชนของฟิสิกส์ต้องการช่วงเวลาคงที่ (Fixed Tick Rate เช่น 60 Hz) หากไปเขียนใน _process ความเร็วและแรงตอบสนองจะขึ้นลงตาม Frame Rate ของหน้าจอ ทำให้ตัวละครเกิดอาการเดินทะลุกำแพงหรือการชนกระตุก (Physics Stutter) ได้ครับ

ข้อ 2: ถ้าเราพยายามเขียน $ChildNode.position = Vector2.ZERO ด้านในฟังก์ชัน _init() จะเกิดผลลัพธ์อย่างไร?
กดดูเฉลยข้อ 2
คำตอบ: จะเกิด Runtime Crash: Null-Pointer Error ทันที!

เพราะในขณะที่เอนจินรัน _init() วัตถุเพิ่งจะเริ่มจอง RAM และยังไม่ได้ถูกนำเข้าสู่ SceneTree ทำให้ความสัมพันธ์ของ Parent และ Node ลูก ($ChildNode) ยังไม่มีอยู่จริง การอ้างอิงผ่าน $ จึงส่งค่า null กลับมาและแครชทันที ต้องย้ายไปทำใน _ready() หรือใช้ @onready เท่านั้นครับ

เตรียมตัวสำหรับวันถัดไป

พร้อมต่อ Day 4 แล้วหรือยังครับ?

วันพรุ่งนี้เราจะเจาะลึก Day 4: Robust Node Access & Scene Unique Nodes (%) กำจัดปัญหาเส้นทางพังเปราะบาง $A/B/C/D ด้วยฟีเจอร์ % ของ Godot 4!

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