5.2 Blueprints

เฟส 5 · Unreal Engine (ทางเลือก) · เวลาเรียน: 25–45 h

visual scripting ของ Unreal — prototype เร็วและเขียน gameplay โดยไม่ต้องเขียน C++ ใช้กันมากแม้ในสตูดิโอ AAA

ก่อนหน้านี้ในบทนี้คุณเห็นแล้วว่า object ของ Unreal ทำงานยังไงใน C++: class ที่ inherit จาก AActor มี variable และ function ที่ถูก expose ออกมาด้วย macro อย่าง UPROPERTY และ UFUNCTION หัวข้อนี้จะพูดถึงเครื่องมือที่ developer สาย Unreal ใช้บ่อยพอ ๆ กัน บางทีบ่อยกว่าด้วยซ้ำ นั่นคือ Blueprint ระบบ visual scripting (การเขียนโปรแกรมด้วยภาพแทนตัวหนังสือ) ของ Unreal Engine เมื่อจบหัวข้อนี้ คุณจะอ่าน Blueprint graph ได้เหมือนอ่านหน้า C++ หนึ่งหน้า รู้ว่าเมื่อไหร่ควรใช้อันไหน และเข้าใจว่าทั้งสองคุยกันยังไงในโปรเจกต์จริง

ทุกหัวข้อด้านล่างจะเป็นรูปแบบเดียวกับบทก่อน ๆ: ตัวอย่างสั้น ๆ, การ trace ว่าเกิดอะไรขึ้นจริง แล้วค่อยอธิบายแบบเข้าใจง่าย Blueprint ไม่ print output ออก console เหมือน C++ ดังนั้นแทนที่จะเป็น "output จริง" คุณจะเห็นการ trace graph — node ไหนทำงาน ตามลำดับไหน ด้วยค่าอะไร

1. Blueprint คืออะไร?

Blueprint คือระบบ visual scripting ของ Unreal Engine แทนที่จะพิมพ์โค้ด C++ เป็นบรรทัด คุณวางกล่องเล็ก ๆ เรียกว่า node ลงบน canvas แล้วเชื่อมกันด้วยเส้นเรียกว่า wire แต่ละ node ทำงานเล็ก ๆ อย่างเดียว — "ตอนเกมเริ่ม," "บวกเลขสองตัว," "เล่นเสียง" ส่วน wire บอกว่าอะไรเกิดก่อนหลัง และข้อมูลไหลไปทางไหน

ลึกลงไปแล้ว Blueprint ก็ยังกลายเป็น bytecode จริง ๆ (คำสั่งที่ compile แล้วซึ่ง virtual machine ของ engine รันได้) ตอนที่คุณกด Compile คุณไม่ได้ "ปลอม" การเขียนโค้ด — คุณกำลังเขียนโค้ดด้วยกล่องกับเส้นแทนตัวหนังสือ เกือบทุกอย่างที่สร้างใน Blueprint graph ได้ ในทางทฤษฎีก็เขียนเป็น C++ ได้เหมือนกัน คุณค่าของ Blueprint ไม่ใช่ว่ามันทำอะไรที่ C++ ทำไม่ได้ แต่คือมันอ่านง่ายกว่า เปลี่ยนง่ายกว่า และไม่ต้องใช้ C++ compiler กับการ rebuild โปรเจกต์เพื่อทดสอบ

TEXT CODE (C++) VISUAL CODE (Blueprint) Score += 10; [Add Score: 10] PlaySound(CoinSound); | v [Play Sound: CoinSound]

ทั้งสองฝั่งด้านบนทำงานเหมือนกัน ฝั่งซ้ายคือตัวหนังสือที่คุณ compile ด้วย C++ compiler ฝั่งขวาคือ graph ที่คุณ compile ด้วยการกดปุ่มใน editor — ไม่ต้องมี build tool แยก ไม่ต้องรอ C++ module ทั้งก้อน relink

ทัวร์คร่าว ๆ ของ Blueprint Editor

เวลาคุณเปิด Blueprint (เช่น double-click BP_Coin ใน Content Browser) คุณจะเห็นหลาย panel มือใหม่ไม่ต้องจำให้ครบทุกอัน แต่ควรรู้จักพวกนี้ไว้:

THE BLUEPRINT EDITOR (simplified layout) My Blueprint panel Event Graph (the canvas) ------------------- -------------------------- Variables [Event BeginPlay] Score (int) | exec bCollected (bool) v Functions [Print String] AddScore Graphs EventGraph ConstructionScript Details panel (bottom): shows properties of whatever is currently selected above.
Tip คลิกขวาที่ไหนก็ได้บน Event Graph canvas แล้วพิมพ์เพื่อค้นหา node ตามชื่อ — ไม่ต้องจำว่า node อยู่ menu ไหน หลังเชื่อม wire เสร็จ กด Compile ที่ toolbar ถ้า node ไหนกลายเป็นสีแดงพร้อมไอคอนเตือน แปลว่ามีปัญหา เอา mouse วางค้างไว้จะบอกว่าขาดอะไร

2. ทำไมแม้แต่สตูดิโอ AAA ก็ยังใช้ Blueprint

มือใหม่บางคนคิดว่า Blueprint เป็นเวอร์ชัน "ของเล่น" สำหรับ hobbyist และสตูดิโอมืออาชีพใช้แต่ C++ นั่นไม่จริง Fortnite, Gears of War และเกม Unreal ระดับ AAA ส่วนใหญ่ shipped ด้วย Blueprint graph หลายพันอันในเกมจริง มีเหตุผลที่จับต้องได้ดังนี้:

รูปแบบที่ใช้จริงในวงการไม่ใช่ "C++ หรือ Blueprint" แต่เป็น "C++ สำหรับส่วนที่ต้องเร็วหรือถูกใช้ซ้ำทุกที่ Blueprint สำหรับส่วนที่ต้องเปลี่ยนบ่อย" หัวข้อ 8 กับ 9 จะลงรายละเอียดเรื่องนี้

3. Event Node: จุดเริ่มต้นของ Graph

ทุกสาย node ใน Blueprint ต้องเริ่มจากที่ไหนสักที่ มันเริ่มที่ Event node — node ที่ไม่มี execution input มีแต่ execution output ซึ่งจะทำงานเองอัตโนมัติเมื่อมีอะไรบางอย่างเกิดขึ้น Event node จะถูกวาดเป็นสีแดงใน editor

Event node ที่มือใหม่จะใช้บ่อยที่สุด:

[Event BeginPlay] [Event Tick] | Delta Seconds (float) o v | runs once, v right after spawn runs every single frame; Delta Seconds = seconds since the last frame
Common mistake มือใหม่ Blueprint มักเอา logic ที่กินเวลาเยอะ (หา actor, วน loop array ขนาดใหญ่, spawn object) ไปใส่ตรง ๆ ใน Event Tick Tick ทำงาน 30 ถึง 240+ ครั้งต่อวินาทีขึ้นอยู่กับ frame rate logic ที่ไม่จำเป็นต้องรันทุกเฟรมไม่ควรอยู่ใน Tick — ดูหัวข้อ 11

4. Execution Wire กับ Data Wire

node หนึ่งตัวมีจุดเชื่อมต่อได้สูงสุดสี่แบบ เรียกว่า pin:

pin สามเหลี่ยมสีขาวเชื่อมกันด้วย execution wire (เรียกสั้น ๆ ว่า exec wire) นี่คือ "แล้วต่อด้วย" ของ graph — กำหนดลำดับ เหมือนกับที่ semicolon และการขึ้นบรรทัดใหม่กำหนดลำดับใน C++ ส่วน pin วงกลมมีสีเชื่อมกันด้วย data wire — คือ "ใช้ค่านี้" ของ graph เหมือนกับตัวแปรหรือ expression ที่ส่งเป็น argument ใน C++

สีของ pin บอกชนิดข้อมูล เหมือนกับที่ type ใน C++ บอกว่าตัวแปรเก็บอะไร:

EXECUTION WIRE (white, thick) — defines ORDER [Node A] ====> [Node B] ====> [Node C] DATA WIRE (colored, thin) — supplies a VALUE [Get Score] ----(int)----> [Print String]

data input pin ที่ไม่มี wire ต่ออยู่ก็ยังทำงานได้ — มันใช้ค่า default ที่คุณพิมพ์ตรง ๆ ลงใน pin (แสดงเป็นกล่องข้อความเล็ก ๆ บน node เอง) พอคุณต่อ wire เข้าไป กล่อง default นั้นจะหายไป กลายเป็น wire ที่ส่งค่าให้แทน

5. Variable ใน Blueprint

variable ของ Blueprint ทำงานเหมือน member variable ใน class ของ C++ — เก็บค่าที่เป็นของ object นี้ อ่านหรือเปลี่ยนตรงไหนของ graph ก็ได้ และเก็บค่าไว้ข้ามเฟรม

คุณสร้างมันใน My Blueprint panel โดยกด + ข้าง Variables ตั้งชื่อ แล้วเลือก type (Boolean, Integer, Float, Vector, Actor Reference และอื่น ๆ — แนวคิดเดียวกับการเลือก type ใน C++ แค่เลือกจาก dropdown แทนการพิมพ์ int32 หรือ bool)

เวลาจะใช้ variable ใน graph ให้ลากมันจาก My Blueprint panel ลงบน canvas editor จะถามว่าคุณอยากได้ node Get (อ่านค่า) หรือ node Set (เขียนค่า)

GET NODE (pure — reads a value, no exec pins) +----------------+ | Get Score | | Score o-----|---> (hand back the value, instantly) +----------------+ SET NODE (impure — writes a value, has exec pins) exec in | v +--------------------+ | Set Score | | New Value o<---------|--- (value to store) | Score o--------|--> (also outputs the new value) +--------------------+ | v exec out

สังเกตว่า node Get ไม่มี pin สามเหลี่ยมสีขาวเลยสักอัน การอ่าน variable ไม่ได้ "ทำอะไร" ในแง่ของลำดับ มันแค่ส่งค่ากลับมาทันที node แบบนี้เรียกว่า pure ส่วน node Set เปลี่ยนอะไรบางอย่าง (มันเขียนลง memory) เลยต้องมี exec pin เพื่อบอกว่า เมื่อไหร่ ในลำดับที่การเขียนนั้นเกิดขึ้น เรียกว่า impure

มี checkbox สองอันที่สำคัญสำหรับ variable ดูได้ใน Details panel หลังเลือก variable นั้น:

6. Function, Custom Event, Branch และ Loop

สี่ node type นี้คือ building block ของ "logic" จริง ๆ ใน graph — เวอร์ชันภาพของการนิยาม function, callback, if และ loop ใน C++

Function

Blueprint Function คือแนวคิดเดียวกับ function ใน C++: กลุ่ม node ที่มีชื่อ ใช้ซ้ำได้ รับ input ได้ ส่ง output กลับได้ (ไม่จำเป็นต้องมี) และเรียกได้จากหลายที่ คุณสร้างมันใน My Blueprint panel ใต้ Functions แล้วสร้าง graph ของมันแบบเดียวกับ Event Graph ต่างกันตรงที่ Function เริ่มด้วย node Entry (รายการ parameter ของมัน) แทนที่จะเป็น Event node

// The C++ equivalent of a Blueprint function "AddScore"
void AGameCharacter::AddScore(int32 Amount)
{
    Score += Amount;
    if (Score >= 100 && !bHasWon)
    {
        bHasWon = true;
        OnPlayerWon();
    }
}
FUNCTION GRAPH: "AddScore" [Entry] Amount (int) o | exec v [Set Score = Score + Amount] | exec v [Branch] Condition o<--- (Score >= 100 AND NOT bHasWon) | True | False | v v [Set bHasWon = true] (nothing else happens) | exec v [Call OnPlayerWon]

Function ก็ตั้งเป็น Pure ได้เหมือนกัน (ไม่มี exec pin เหมือน node Get ในหัวข้อ 5) ถ้ามันแค่คำนวณแล้วคืนค่าโดยไม่เปลี่ยนอะไรเลย — เช่น function ที่คืน true ถ้า player มีทองพอซื้อไอเทม โดยไม่ได้หักทองจริง ๆ

Custom Event

node Custom Event หน้าตาคล้าย Function แต่ถูกวางตรง ๆ บน Event Graph และ เรียกจาก Blueprint อื่นได้ หรือถูก trigger จาก Event Dispatcher (หัวข้อ 10) กฎง่าย ๆ: ใช้ Function เมื่อต้องการค่า return หรือเรียกจากใน Blueprint นี้เท่านั้น ใช้ Custom Event เมื่อมีอะไรอื่น ต้องมา trigger มัน หรือมันเข้ากับความหมาย "เหตุการณ์ที่เกิดขึ้น" มากกว่า "ค่าที่ฉันคำนวณ"

Branch กับ Loop ในฐานะ Node

keyword ควบคุมลำดับของ C++ อย่าง if, for และ while กลายเป็น node เฉพาะใน Blueprint:

BRANCH — the Blueprint "if" [Branch] Condition o | True | False | v v [Do Thing A] [Do Thing B] FORLOOP — runs body once per index, 0 to 4 [ForLoop] First Index: 0 Last Index: 4 | Loop Body | Completed | v v [Print Index] [Print "Done"]
Common mistake ลืมต่ออะไรเข้ากับ pin Completed ของ loop ไม่มีผลเสีย (แปลว่าไม่มีอะไรเกิดขึ้นหลังจากนั้น) ที่ทำให้คนพลาดคือลืมว่า Loop Body ทำงาน หนึ่งครั้งต่อ element ก่อนจะไปตัวถัดไป — รวมถึงสาย exec ที่ต่ออยู่ด้วย ถ้าสายนั้นทำอะไรที่ช้า เช่น spawn actor การทำแบบนั้น 10,000 ครั้งในเฟรมเดียวภายใน ForEachLoop จะทำให้เกม freeze ไปทั้งเฟรม

Sequence: หลายเส้นทางพร้อมกัน

อีก node ควบคุมลำดับที่ควรรู้จักไว้: Sequence มี exec input หนึ่งอัน กับ exec output ที่มีเลขกำกับหลายอัน (Then 0, Then 1, Then 2...) ซึ่งทั้งหมดทำงานตามลำดับ ทีละอัน ในเฟรมเดียวกัน มันไม่ใช่ loop — มันเป็นวิธีบอกว่า "ทำทั้งหมดนี้ ตามลำดับนี้" มีประโยชน์เวลา node มี exec output pin แค่อันเดียว แต่คุณต้องเริ่มหลายสายพร้อมกันจากมัน

7. เดินผ่านตัวอย่าง: Graph ของการเก็บเหรียญ

ตอนนี้เอาหัวข้อ 3 ถึง 6 มารวมกันเป็นตัวอย่างจริงหนึ่งอัน: เหรียญที่ player เดินผ่านแล้วเก็บได้ graph นี้อยู่ใน Blueprint ชื่อ BP_Coin ซึ่งเป็น Actor ที่มี sphere collision component กับ static mesh (โมเดลของเหรียญที่มองเห็น)

เป้าหมาย: เมื่อ player overlap กับเหรียญ ให้บวกคะแนนของ player เพิ่ม 10, เล่นเสียงเก็บของ แล้วลบเหรียญทิ้ง — แต่ทำแค่ครั้งเดียว แม้ overlap event จะเกิดสองครั้งขึ้นมาเฉย ๆ

EVENT GRAPH — Blueprint "BP_Coin" [Event ActorBeginOverlap] Other Actor o (not wired below; this example does not check who) | exec v [Branch] Condition o<--- [NOT] <--- [Get bCollected] | True | False | v v [Set bCollected = true] (exec chain stops here, | exec nothing else happens) v [Call AddScore] Target o<--- [Get PlayerCharacterRef] Amount: 10 | exec v [Play Sound at Location] Sound o<--- CoinPickupSound (asset) Location o<--- [Get Actor Location] (self) | exec v [Destroy Actor] Target o<--- (self, default)

อ่านแบบเดียวกับที่คุณอ่าน C++ ทีละบรรทัด:

ทุก node ตรงนี้ตรงกับสิ่งที่คุณรู้จักอยู่แล้วจาก C++: statement if, boolean flag ที่ใช้เป็นตัวกันครั้งเดียว, การเรียก function และการ cleanup ตอนท้าย scope รูปแบบภาพแค่วาดลำดับกับการไหลของข้อมูลให้เห็นตรง ๆ แทนที่คุณจะต้องนั่งไล่จาก indentation กับ semicolon

Common mistake ปล่อย pin Target ของการเรียก function ไว้ไม่ต่อ wire จะถือว่าเป็น "Self" เงียบ ๆ — object ที่เป็นเจ้าของ graph นี้ ใน [Call AddScore] ด้านบน ถ้าลืมต่อ Target ไปที่ Get PlayerCharacterRef node จะพยายามเรียก AddScore บนตัวเหรียญเอง เพราะ BP_Coin ไม่มี function แบบนั้น graph จะ compile ไม่ผ่าน แล้ว error จะชี้ไปที่ pin นั้น

8. Blueprint vs. C++: เลือกเครื่องมือให้ถูก

นี่คือคำถามที่มือใหม่ถามบ่อยที่สุดหลังเห็นทั้งสองแบบ ไม่มีกฎตายตัวข้อเดียว แต่มีสัญญาณที่ชัดเจนสำหรับแต่ละฝั่ง

เอนไปทาง C++ เมื่อ:

เอนไปทาง Blueprint เมื่อ:

Ask: how OFTEN does this change, and how much PERFORMANCE / SCALE does it need? Changes often, low performance need --> BLUEPRINT "this specific puzzle trigger in level 3" "boss enters phase 2 when HP < 30%" Changes rarely, high performance need --> C++ "pathfinding for 200 AI agents" "inventory system core logic" "network replication of player position"

9. Hybrid Workflow ในทางปฏิบัติ

ในโปรเจกต์ Unreal จริง คุณแทบไม่เคยเลือก "Blueprint ทั้งหมด" หรือ "C++ ทั้งหมด" สำหรับทั้งเกม รูปแบบมาตรฐานคือ: เขียน C++ base class ที่แบกงานหนัก แล้วสร้าง Blueprint ที่ inherit จากมัน มาเติมรายละเอียดเฉพาะของแต่ละ content

// AGameCharacter.h -- the C++ base class
UCLASS()
class MYGAME_API AGameCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Score")
    int32 Score = 0;

    UFUNCTION(BlueprintCallable, Category = "Score")
    void AddScore(int32 Amount);

    // BlueprintImplementableEvent: C++ declares this, but the BODY
    // is drawn as a graph inside a Blueprint subclass.
    UFUNCTION(BlueprintImplementableEvent, Category = "Score")
    void OnScoreChanged(int32 NewScore);
};

มี macro สามตัวที่ทำให้ C++ กับ Blueprint คุยกันได้ มือใหม่ทุกคนควรจำหน้าตามันไว้:

C++ BASE CLASS: AGameCharacter BLUEPRINT SUBCLASS: BP_Player (inherits from AGameCharacter) Score (int) ---exposed--> can Get / Set Score AddScore(Amount) ---exposed--> can place [Add Score] node OnScoreChanged(NewScore) --declared only, no body--> [Event OnScoreChanged] NewScore (int) o | exec v [Flash UI Widget] | exec v [Play "ding" Sound]

การแบ่งแบบนี้แปลว่า programmer เป็นเจ้าของ Score, AddScore และอะไรก็ตามที่ต้องเร็วหรือต้องถูกต้อง ส่วน designer เป็นเจ้าของว่าหน้าจอทำอะไรเมื่อคะแนนเปลี่ยน ซึ่งถูกปรับตลอดระหว่าง playtest และไม่ต้องมี programmer มาคอยวนอยู่ในลูปทุกครั้งที่เปลี่ยน

Tip ตัวแปรที่พบบ่อยมากคือใช้ BlueprintNativeEvent แทน BlueprintImplementableEvent มันให้ C++ ใส่ body เริ่มต้นได้ และ ให้ Blueprint override มันได้ถ้าต้องการ ลองไปหาอ่านต่อเมื่อคุณคุ้นกับ pattern ด้านบนแล้ว

10. Blueprint คุยกันเอง

ตัวอย่างเหรียญในหัวข้อ 7 เรียก AddScore บน "ตัว player" แล้ว BP_Coin ได้ reference ของ Blueprint player มาเรียก function บนมันได้ยังไง? มีสามวิธีที่ใช้กันบ่อย เรียงจากตายตัวสุดไปยืดหยุ่นสุด

1. Direct reference

ถ้า Blueprint มี variable type Actor Reference (หรือ type ที่เจาะจงกว่าอย่าง BP_PlayerCharacter) ที่ชี้ไปยัง object อื่นตัวใดตัวหนึ่งโดยเฉพาะ มันเรียก function ของ object นั้นได้ตรง ๆ — เหมือนกับที่ pointer ใน C++ ให้คุณเรียก method บน object อื่นได้ ง่าย แต่ผูกกันแน่นมาก: BP_Coin ต้องรู้ว่า class BP_PlayerCharacter มีอยู่จริง ซึ่งจะพังทันทีถ้าคุณเอา BP_Coin ไปใช้ซ้ำในโปรเจกต์ที่มี player class ต่างออกไป

2. Blueprint Interface

Blueprint Interface คือรายการ function ที่มีชื่อ ไม่มี implementation ที่ Blueprint ไหนก็ "implement" ได้ น่าจะคุ้น ๆ — เป็นแนวคิดเดียวกับ abstract base class ใน C++ ที่มี pure virtual function หรือ interface ของ C# BP_Coin เรียก "Give Score" บน อะไรก็ตาม ที่มา overlap มันได้ ตราบใดที่ actor นั้น implement interface BPI_ScoreReceiver โดยไม่ต้องรู้หรือสนใจเลยว่ามันเป็น class ไหนกันแน่

// The C++ equivalent of a Blueprint Interface
UINTERFACE(BlueprintType)
class UScoreReceiver : public UInterface
{
    GENERATED_BODY()
};

class IScoreReceiver
{
    GENERATED_BODY()
public:
    UFUNCTION(BlueprintCallable, Category = "Score")
    void GiveScore(int32 Amount);
};
BP_Coin some actor implementing BPI_ScoreReceiver [Event ActorBeginOverlap] Other Actor o | exec v [Does Implement Interface?] Interface: BPI_ScoreReceiver Target o<--- Other Actor | Yes | No | v v [Call Interface Function: (do nothing -- this actor "Give Score", Amount: 10] does not receive score) Target: Other Actor]

3. Event Dispatcher

Event Dispatcher คือวิธีที่ Blueprint บอกว่า "มีอะไรบางอย่างเกิดขึ้น และฉันไม่สนว่าใครฟังอยู่" เป็นแนวคิดเดียวกับ multicast delegate ใน C++ หรือ event ใน C# Blueprint ที่เป็นเจ้าของ dispatcher จะ call (fire) มัน Blueprint อื่นกี่ตัวก็ได้ bind เข้ากับมันไว้ล่วงหน้าเพื่อรับแจ้งตอนมัน fire object ที่ fire dispatcher ไม่ต้องมี reference ไปยัง listener เลยด้วยซ้ำ — listener เป็นฝ่าย subscribe เข้ามาเอง ไม่ใช่ในทางกลับกัน

// The C++ equivalent of a Blueprint Event Dispatcher
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCoinCollected, int32, Amount);

UCLASS()
class MYGAME_API ABP_Coin : public AActor
{
    GENERATED_BODY()

public:
    UPROPERTY(BlueprintAssignable, Category = "Score")
    FOnCoinCollected OnCoinCollected;
};
BP_Coin (the "publisher") [event: OnCoinCollected fires] | +---------------------------+ | | v v BP_HUD (bound listener) BP_AudioManager (bound listener) [Update Score Text] [Play Fanfare Sound] Neither listener needed a reference to BP_Coin ahead of time, except to call "Bind Event to OnCoinCollected" once, usually during BeginPlay.

ใช้ direct reference สำหรับความสัมพันธ์แบบแน่นและครั้งเดียว (ประตูที่เปิดให้สวิตช์เฉพาะตัวเดียวเท่านั้น) ใช้ Interface เมื่อหลาย class ต้องตอบสนองการเรียกเดียวกันในแบบของตัวเอง ใช้ Event Dispatcher เมื่อ object ที่ตรวจจับอะไรบางอย่างไม่ควรต้องรู้ว่าใครสนใจมันบ้าง — เหรียญไม่ควรต้องรู้ด้วยซ้ำว่า HUD มีอยู่

11. Performance: อะไรที่กินทรัพยากรจริง ๆ

node ของ Blueprint รันช้ากว่า C++ ที่เขียนมือแบบเทียบเท่ากัน เพราะแต่ละ node แบก bookkeeping เพิ่ม (virtual machine ของ engine ตีความ graph แทนที่จะรัน machine instruction ดิบ ๆ) สำหรับ gameplay glue ส่วนใหญ่ — ตอบสนอง overlap ครั้งเดียว, เรียก function ตอนกดปุ่ม — ต้นทุนนี้เล็กเกินกว่าจะสำคัญ มันจะกลายเป็นปัญหาจริงในบางสถานการณ์เฉพาะ:

// Getting overlapping actors returns an array -- cheap once,
// expensive if you call this every frame for many objects.
TArray<AActor*> OverlappingActors;
GetOverlappingActors(OverlappingActors, ABP_PlayerCharacter::StaticClass());

if (OverlappingActors.Num() > 0 && !bCollected)
{
    bCollected = true;
}
Tip engine มี profiler ในตัว (stat unit, stat game และเครื่องมือ Unreal Insights) ที่แสดงเป๊ะ ๆ ว่าเวลาแต่ละเฟรมหมดไปกับอะไร อย่าเดาเรื่อง performance — วัดก่อน แล้วค่อย optimize จุดที่เป็น bottleneck จริง ๆ นี่คือวินัย "measure, don't guess" เดียวกับที่เจอในบทก่อน ๆ เรื่อง debugging

อย่าลืมว่า hybrid pattern จากหัวข้อ 9 เป็นเครื่องมือด้าน performance ในตัวมันเองด้วย เมื่อพบว่า Blueprint graph ตัวไหน hot (รันบ่อย) และช้าไปพร้อมกัน ทางแก้มักไม่ใช่ทิ้ง Blueprint ไปทั้งหมด — แต่คือย้าย function ตัวนั้นตัวเดียวไปเขียนเป็น C++ แล้ว expose ด้วย BlueprintCallable เก็บทุกอย่างรอบ ๆ มันไว้เหมือนเดิม

12. Glossary

13. แบบฝึกหัด

Exercise 1 ออกแบบ Event Graph ให้ประตูที่ล็อกไว้ ข้อกำหนด: ขณะที่ player ยืนอยู่ใน trigger volume ของประตู การกดปุ่ม E ควรเปิดประตูถ้า player มีกุญแจ (variable Boolean ชื่อ bHasKey บนตัว player) หรือ print string "Locked!" ถ้าไม่มี การกด E ตอนไม่ได้อยู่ใกล้ประตูควรไม่มีอะไรเกิดขึ้น อธิบาย (เป็นคำพูดหรือ ASCII diagram แบบที่ใช้ในหัวข้อนี้) ว่าต้องใช้ Event node ไหน และ Branch logic ควรต่อสายยังไง
Show answer

event InputAction เพียงอย่างเดียวบอกไม่ได้ว่า player อยู่ใกล้ประตูหรือเปล่า — การกดปุ่มเป็น global graph จึงต้องมี Boolean flag ที่ track ว่า "player อยู่ในระยะตอนนี้ไหม" ตั้งค่าโดย overlap event ของประตูเอง แล้วค่อยเช็คตอนกดปุ่ม

[Event ActorBeginOverlap] | exec v [Set bInRange = true] [Event ActorEndOverlap] | exec v [Set bInRange = false] [InputAction: E] (Pressed) | exec v [Branch] Condition o<--- bInRange | True | False | v v [Branch] (do nothing -- not Condition o<--- bHasKey near the door) | True | False | v v [Open Door] [Print String: "Locked!"]

มี Event node แยกกันสามอันป้อนเข้า graph นี้: overlap event สองอันแค่คอย maintain flag bInRange ไม่ทำอะไรอย่างอื่น ส่วน input event เป็นคนทำงานจริง แต่ทำหลังเช็ค bInRange ก่อน แล้วค่อยเช็ค bHasKey ที่ซ้อนอยู่ข้างใน Branch สองอันต่อกันแบบนี้ทำงานเหมือน if (bInRange) { if (bHasKey) { ... } else { ... } } ใน C++

Exercise 2 เพื่อนร่วมทีมต่อสาย graph เก็บเหรียญไว้แล้วมารายงานว่า "เสียงเล่นและเหรียญหายไปทุกครั้ง แต่คะแนนไม่เพิ่มเลย" นี่คือสิ่งที่เขาต่อไว้ หา bug แล้วอธิบายวิธีแก้
[Event ActorBeginOverlap] | exec v [Branch] Condition o<--- NOT bCollected | True | v [Set bCollected = true] | exec v [Play Sound at Location] [Call AddScore] | exec Amount: 10 v (sitting on the canvas, [Destroy Actor] nothing wired into it)
Show answer

node [Call AddScore] มี data pin ที่ใส่ค่าถูกต้อง (Amount: 10) แต่ไม่มี execution wire ต่อเข้าไปเลย execution flow ไหลตรงจาก Set bCollected = true ไปที่ Play Sound at Location เลย ข้าม Call AddScore ไปทั้งดุ้น node ที่ไม่มี exec wire ต่อเข้าไปจะไม่ทำงาน ไม่ว่า data pin ของมันจะถูกต้องแค่ไหน — data pin แค่ส่งค่าให้ ไม่ได้ trigger อะไรด้วยตัวมันเอง

วิธีแก้: ลาก execution wire ใหม่จาก exec output ของ Set bCollected = true เข้าไปที่ exec input ของ Call AddScore แล้วจาก exec output ของ Call AddScore เข้าไปที่ exec input ของ Play Sound at Location เพื่อคืนสายให้ครบ: Branch (True) -> Set bCollected -> Call AddScore -> Play Sound -> Destroy Actor

Exercise 3 สำหรับแต่ละ feature ด้านล่าง ตัดสินใจว่า: C++ ล้วน, Blueprint ล้วน หรือ C++ base class กับ Blueprint subclass (hybrid)? ให้เหตุผลหนึ่งประโยคต่อข้อ
  • (a) logic pathfinding ที่ AI ศัตรู 200 ตัวในเลเวลใช้ร่วมกัน
  • (b) trigger เฟส 2 ของบอสตัวหนึ่งโดยเฉพาะ: "เมื่อ HP ต่ำกว่า 30% ให้เล่น cutscene นี้"
  • (c) logic การเคลื่อนที่และกระโดดหลักของตัวละคร player ที่ตัวละครเล่นได้ทุกตัวต้องมี แต่แต่ละตัวตอบสนอง event (การลงพื้น, การโดนดาเมจ) ต่างกันเล็กน้อยสำหรับ VFX กับเสียง
  • (d) ปริศนาเฉพาะจุดในเลเวล 7 ที่ต้องกดสวิตช์สามตัวตามลำดับที่ถูกต้อง
  • (e) ระบบ save/load ที่ serialize ความคืบหน้าของ player ลงดิสก์
Show answer

(a) C++. มันรันตลอดเวลา บน agent จำนวนมาก และเป็น algorithm ที่นิ่งเมื่อเขียนเสร็จแล้ว -- ตรงกับกรณี "เปลี่ยนน้อย ต้องการ performance สูง" จากหัวข้อ 8

(b) Blueprint. มันเจาะจงกับ encounter เดียว จะถูกปรับตลอดระหว่าง playtest ("HP < 30%? บางที 25% อาจจะรู้สึกดีกว่า") และไม่ต้องเร็ว -- มัน fire แค่ครั้งเดียว

(c) Hybrid. คณิตศาสตร์การเคลื่อนที่หลักควรอยู่ใน C++ base class เพื่อให้ตัวละครทุกตัวใช้ implementation เดียวกันที่ถูกต้องและเร็ว ส่วนปฏิกิริยาเฉพาะตัวละคร (VFX, เสียง) ประกาศใน C++ เป็น function แบบ BlueprintImplementableEvent แล้วให้ Blueprint subclass ของแต่ละตัวละครใส่ body เอง -- pattern เดียวกับหัวข้อ 9 เป๊ะ ๆ

(d) Blueprint. มันเป็น content เฉพาะจุดของเลเวลเดียว น่าจะสร้างและปรับโดย designer ไม่มีเรื่อง performance ต้องกังวล

(e) C++. การ serialize ต้องถูกต้อง เร็ว และนิ่ง -- bug ตรงนี้ทำให้ save file ของ player พังได้ และ format ก็ไม่ค่อยเปลี่ยนเมื่อ settle แล้ว

ตอนนี้คุณอ่าน Blueprint graph ได้แบบเดียวกับที่อ่าน C++: event เริ่มสายงาน, exec wire สีขาวกำหนดลำดับ, data wire มีสีพาค่าไป, variable กับ function ทำงานเหมือนคู่ของมันใน C++ และ Branch/loop ก็แค่ if/for/while ที่วาดเป็นกล่อง คุณยังรู้คำถามที่สำคัญกว่า "Blueprint หรือ C++" ด้วย: system จริง ๆ ส่วนใหญ่เป็นทั้งคู่ C++ base class แบกส่วนที่ต้องเร็วและนิ่ง Blueprint subclass แบกส่วนที่เปลี่ยนทุก playtest -- คุยกันผ่าน direct reference, interface หรือ event dispatcher ขึ้นอยู่กับว่าควรผูกกันแน่นแค่ไหน

← กลับไปหน้ารวมบท