ก่อนหน้านี้ในบทนี้คุณเห็นแล้วว่า 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 ไหนทำงาน ตามลำดับไหน ด้วยค่าอะไร
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 โปรเจกต์เพื่อทดสอบ
ทั้งสองฝั่งด้านบนทำงานเหมือนกัน ฝั่งซ้ายคือตัวหนังสือที่คุณ compile ด้วย C++ compiler ฝั่งขวาคือ graph ที่คุณ compile ด้วยการกดปุ่มใน editor — ไม่ต้องมี build tool แยก ไม่ต้องรอ C++ module ทั้งก้อน relink
เวลาคุณเปิด Blueprint (เช่น double-click BP_Coin ใน Content Browser) คุณจะเห็นหลาย panel มือใหม่ไม่ต้องจำให้ครบทุกอัน แต่ควรรู้จักพวกนี้ไว้:
มือใหม่บางคนคิดว่า Blueprint เป็นเวอร์ชัน "ของเล่น" สำหรับ hobbyist และสตูดิโอมืออาชีพใช้แต่ C++ นั่นไม่จริง Fortnite, Gears of War และเกม Unreal ระดับ AAA ส่วนใหญ่ shipped ด้วย Blueprint graph หลายพันอันในเกมจริง มีเหตุผลที่จับต้องได้ดังนี้:
รูปแบบที่ใช้จริงในวงการไม่ใช่ "C++ หรือ Blueprint" แต่เป็น "C++ สำหรับส่วนที่ต้องเร็วหรือถูกใช้ซ้ำทุกที่ Blueprint สำหรับส่วนที่ต้องเปลี่ยนบ่อย" หัวข้อ 8 กับ 9 จะลงรายละเอียดเรื่องนี้
ทุกสาย node ใน Blueprint ต้องเริ่มจากที่ไหนสักที่ มันเริ่มที่ Event node — node ที่ไม่มี execution input มีแต่ execution output ซึ่งจะทำงานเองอัตโนมัติเมื่อมีอะไรบางอย่างเกิดขึ้น Event node จะถูกวาดเป็นสีแดงใน editor
Event node ที่มือใหม่จะใช้บ่อยที่สุด:
Event BeginPlay — ทำงานครั้งเดียว ตอนที่ object นี้ถูก spawn เข้ามาใน level และพร้อมแล้ว เทียบเท่ากับ Start() ของ UnityEvent Tick — ทำงานทุกเฟรม และส่ง Delta Seconds (เวลาเป็นวินาทีตั้งแต่เฟรมก่อนหน้า) มาให้ เทียบเท่ากับ Update() ของ UnityEvent ActorBeginOverlap / Event ActorEndOverlap — ทำงานเมื่อ collision ของ actor นี้เริ่มหรือเลิกแตะ collision ของ actor อื่นInputAction Jump — ทำงานเมื่อ player กดปุ่มหรือคีย์ที่ bind ไว้Event Tick Tick ทำงาน 30 ถึง 240+ ครั้งต่อวินาทีขึ้นอยู่กับ frame rate logic ที่ไม่จำเป็นต้องรันทุกเฟรมไม่ควรอยู่ใน Tick — ดูหัวข้อ 11node หนึ่งตัวมีจุดเชื่อมต่อได้สูงสุดสี่แบบ เรียกว่า pin:
pin สามเหลี่ยมสีขาวเชื่อมกันด้วย execution wire (เรียกสั้น ๆ ว่า exec wire) นี่คือ "แล้วต่อด้วย" ของ graph — กำหนดลำดับ เหมือนกับที่ semicolon และการขึ้นบรรทัดใหม่กำหนดลำดับใน C++ ส่วน pin วงกลมมีสีเชื่อมกันด้วย data wire — คือ "ใช้ค่านี้" ของ graph เหมือนกับตัวแปรหรือ expression ที่ส่งเป็น argument ใน C++
สีของ pin บอกชนิดข้อมูล เหมือนกับที่ type ใน C++ บอกว่าตัวแปรเก็บอะไร:
bool)int32); เขียวอมฟ้าอีกเฉด = Floatdata input pin ที่ไม่มี wire ต่ออยู่ก็ยังทำงานได้ — มันใช้ค่า default ที่คุณพิมพ์ตรง ๆ ลงใน pin (แสดงเป็นกล่องข้อความเล็ก ๆ บน node เอง) พอคุณต่อ wire เข้าไป กล่อง default นั้นจะหายไป กลายเป็น wire ที่ส่งค่าให้แทน
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 (เขียนค่า)
สังเกตว่า node Get ไม่มี pin สามเหลี่ยมสีขาวเลยสักอัน การอ่าน variable ไม่ได้ "ทำอะไร" ในแง่ของลำดับ มันแค่ส่งค่ากลับมาทันที node แบบนี้เรียกว่า pure ส่วน node Set เปลี่ยนอะไรบางอย่าง (มันเขียนลง memory) เลยต้องมี exec pin เพื่อบอกว่า เมื่อไหร่ ในลำดับที่การเขียนนั้นเกิดขึ้น เรียกว่า impure
มี checkbox สองอันที่สำคัญสำหรับ variable ดูได้ใน Details panel หลังเลือก variable นั้น:
สี่ node type นี้คือ building block ของ "logic" จริง ๆ ใน graph — เวอร์ชันภาพของการนิยาม function, callback, if และ loop ใน C++
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 ก็ตั้งเป็น Pure ได้เหมือนกัน (ไม่มี exec pin เหมือน node Get ในหัวข้อ 5) ถ้ามันแค่คำนวณแล้วคืนค่าโดยไม่เปลี่ยนอะไรเลย — เช่น function ที่คืน true ถ้า player มีทองพอซื้อไอเทม โดยไม่ได้หักทองจริง ๆ
node Custom Event หน้าตาคล้าย Function แต่ถูกวางตรง ๆ บน Event Graph และ เรียกจาก Blueprint อื่นได้ หรือถูก trigger จาก Event Dispatcher (หัวข้อ 10) กฎง่าย ๆ: ใช้ Function เมื่อต้องการค่า return หรือเรียกจากใน Blueprint นี้เท่านั้น ใช้ Custom Event เมื่อมีอะไรอื่น ต้องมา trigger มัน หรือมันเข้ากับความหมาย "เหตุการณ์ที่เกิดขึ้น" มากกว่า "ค่าที่ฉันคำนวณ"
keyword ควบคุมลำดับของ C++ อย่าง if, for และ while กลายเป็น node เฉพาะใน Blueprint:
Branch — เวอร์ชัน Blueprint ของ if มี Boolean data input pin หนึ่งอัน กับ exec output pin สองอันชื่อ True กับ FalseForLoop — รัน body ของมันหนึ่งครั้งต่อ integer แต่ละตัว จาก First Index ถึง Last Index คล้าย for (int i = First; i <= Last; i++) มี exec pin Loop Body (ทำงานทุกรอบ) กับ Completed (ทำงานครั้งเดียว หลัง loop จบ)ForEachLoop — รัน body หนึ่งครั้งต่อ element ของ array คล้าย range-based for (auto& Elem : Array)WhileLoop — รัน body ซ้ำ ๆ ตราบใดที่เงื่อนไข Boolean ยังเป็นจริง คล้าย while (condition) { ... }Completed ของ loop ไม่มีผลเสีย (แปลว่าไม่มีอะไรเกิดขึ้นหลังจากนั้น) ที่ทำให้คนพลาดคือลืมว่า Loop Body ทำงาน หนึ่งครั้งต่อ element ก่อนจะไปตัวถัดไป — รวมถึงสาย exec ที่ต่ออยู่ด้วย ถ้าสายนั้นทำอะไรที่ช้า เช่น spawn actor การทำแบบนั้น 10,000 ครั้งในเฟรมเดียวภายใน ForEachLoop จะทำให้เกม freeze ไปทั้งเฟรมอีก node ควบคุมลำดับที่ควรรู้จักไว้: Sequence มี exec input หนึ่งอัน กับ exec output ที่มีเลขกำกับหลายอัน (Then 0, Then 1, Then 2...) ซึ่งทั้งหมดทำงานตามลำดับ ทีละอัน ในเฟรมเดียวกัน มันไม่ใช่ loop — มันเป็นวิธีบอกว่า "ทำทั้งหมดนี้ ตามลำดับนี้" มีประโยชน์เวลา node มี exec output pin แค่อันเดียว แต่คุณต้องเริ่มหลายสายพร้อมกันจากมัน
ตอนนี้เอาหัวข้อ 3 ถึง 6 มารวมกันเป็นตัวอย่างจริงหนึ่งอัน: เหรียญที่ player เดินผ่านแล้วเก็บได้ graph นี้อยู่ใน Blueprint ชื่อ BP_Coin ซึ่งเป็น Actor ที่มี sphere collision component กับ static mesh (โมเดลของเหรียญที่มองเห็น)
เป้าหมาย: เมื่อ player overlap กับเหรียญ ให้บวกคะแนนของ player เพิ่ม 10, เล่นเสียงเก็บของ แล้วลบเหรียญทิ้ง — แต่ทำแค่ครั้งเดียว แม้ overlap event จะเกิดสองครั้งขึ้นมาเฉย ๆ
อ่านแบบเดียวกับที่คุณอ่าน C++ ทีละบรรทัด:
NOT bCollected ครั้งแรก bCollected เป็น false ดังนั้น NOT false คือ true แล้ว execution ก็ไปทาง TrueBP_Coin ได้ reference ของ player มาโดยไม่ hardcode)ทุก node ตรงนี้ตรงกับสิ่งที่คุณรู้จักอยู่แล้วจาก C++: statement if, boolean flag ที่ใช้เป็นตัวกันครั้งเดียว, การเรียก function และการ cleanup ตอนท้าย scope รูปแบบภาพแค่วาดลำดับกับการไหลของข้อมูลให้เห็นตรง ๆ แทนที่คุณจะต้องนั่งไล่จาก indentation กับ semicolon
Target ของการเรียก function ไว้ไม่ต่อ wire จะถือว่าเป็น "Self" เงียบ ๆ — object ที่เป็นเจ้าของ graph นี้ ใน [Call AddScore] ด้านบน ถ้าลืมต่อ Target ไปที่ Get PlayerCharacterRef node จะพยายามเรียก AddScore บนตัวเหรียญเอง เพราะ BP_Coin ไม่มี function แบบนั้น graph จะ compile ไม่ผ่าน แล้ว error จะชี้ไปที่ pin นั้นนี่คือคำถามที่มือใหม่ถามบ่อยที่สุดหลังเห็นทั้งสองแบบ ไม่มีกฎตายตัวข้อเดียว แต่มีสัญญาณที่ชัดเจนสำหรับแต่ละฝั่ง
ในโปรเจกต์ 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 คุยกันได้ มือใหม่ทุกคนควรจำหน้าตามันไว้:
UPROPERTY(BlueprintReadWrite) หรือ (BlueprintReadOnly) (macro ที่ expose member variable ของ C++ ออกไปให้ engine และถ้าตั้ง flag ถูก ก็ให้ Blueprint ด้วย) — Blueprint graph จะ Get และถ้าอนุญาต Set variable นี้ได้เหมือนกับ variable ที่สร้างตรง ๆ ใน Blueprint เลยUFUNCTION(BlueprintCallable) (macro ที่ expose function ของ C++ ออกไปให้ engine และถ้าตั้ง flag ถูก ก็ให้ Blueprint ด้วย) — function จะปรากฏเป็น node ที่เรียกได้ใน Blueprint graph ไหนก็ได้ นี่คือเหตุผลที่ AddScore ใน graph ตัวอย่างของหัวข้อ 7 มีอยู่เป็น node ได้ ทั้งที่ logic ของมันอยู่ใน C++UFUNCTION(BlueprintImplementableEvent) — ทิศทางย้อนกลับ C++ ประกาศชื่อกับ signature ของ function เท่านั้น ไม่เขียน body เลย Blueprint subclass เป็นคนวาด body ของ node นั้นเป็น graph ทำให้ designer ตัดสินใจได้เองว่า "จะเกิดอะไรขึ้นเมื่อคะแนนเปลี่ยน" (กะพริบ UI, เล่นเสียง jingle) โดยไม่ต้องแตะ C++การแบ่งแบบนี้แปลว่า programmer เป็นเจ้าของ Score, AddScore และอะไรก็ตามที่ต้องเร็วหรือต้องถูกต้อง ส่วน designer เป็นเจ้าของว่าหน้าจอทำอะไรเมื่อคะแนนเปลี่ยน ซึ่งถูกปรับตลอดระหว่าง playtest และไม่ต้องมี programmer มาคอยวนอยู่ในลูปทุกครั้งที่เปลี่ยน
BlueprintNativeEvent แทน BlueprintImplementableEvent มันให้ C++ ใส่ body เริ่มต้นได้ และ ให้ Blueprint override มันได้ถ้าต้องการ ลองไปหาอ่านต่อเมื่อคุณคุ้นกับ pattern ด้านบนแล้วตัวอย่างเหรียญในหัวข้อ 7 เรียก AddScore บน "ตัว player" แล้ว BP_Coin ได้ reference ของ Blueprint player มาเรียก function บนมันได้ยังไง? มีสามวิธีที่ใช้กันบ่อย เรียงจากตายตัวสุดไปยืดหยุ่นสุด
ถ้า Blueprint มี variable type Actor Reference (หรือ type ที่เจาะจงกว่าอย่าง BP_PlayerCharacter) ที่ชี้ไปยัง object อื่นตัวใดตัวหนึ่งโดยเฉพาะ มันเรียก function ของ object นั้นได้ตรง ๆ — เหมือนกับที่ pointer ใน C++ ให้คุณเรียก method บน object อื่นได้ ง่าย แต่ผูกกันแน่นมาก: BP_Coin ต้องรู้ว่า class BP_PlayerCharacter มีอยู่จริง ซึ่งจะพังทันทีถ้าคุณเอา BP_Coin ไปใช้ซ้ำในโปรเจกต์ที่มี player class ต่างออกไป
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);
};
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;
};
ใช้ direct reference สำหรับความสัมพันธ์แบบแน่นและครั้งเดียว (ประตูที่เปิดให้สวิตช์เฉพาะตัวเดียวเท่านั้น) ใช้ Interface เมื่อหลาย class ต้องตอบสนองการเรียกเดียวกันในแบบของตัวเอง ใช้ Event Dispatcher เมื่อ object ที่ตรวจจับอะไรบางอย่างไม่ควรต้องรู้ว่าใครสนใจมันบ้าง — เหรียญไม่ควรต้องรู้ด้วยซ้ำว่า HUD มีอยู่
node ของ Blueprint รันช้ากว่า C++ ที่เขียนมือแบบเทียบเท่ากัน เพราะแต่ละ node แบก bookkeeping เพิ่ม (virtual machine ของ engine ตีความ graph แทนที่จะรัน machine instruction ดิบ ๆ) สำหรับ gameplay glue ส่วนใหญ่ — ตอบสนอง overlap ครั้งเดียว, เรียก function ตอนกดปุ่ม — ต้นทุนนี้เล็กเกินกว่าจะสำคัญ มันจะกลายเป็นปัญหาจริงในบางสถานการณ์เฉพาะ:
Cast To เช็ค type ตอน runtime และกินเวลานิดหน่อย Cast ครั้งเดียวนอก loop แล้วใช้ผลลัพธ์ซ้ำ ถูกกว่า Cast ข้างใน body ของ ForEachLoop ที่รันเป็นพันครั้งมาก// 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;
}
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 เก็บทุกอย่างรอบ ๆ มันไว้เหมือนเดิม
BeginPlay, Tick, overlap, input)if; Boolean input หนึ่งอัน, exec output True/Falsefor ของ C++ (ตาม index), range-based for (ตาม element) และ whileE ควรเปิดประตูถ้า player มีกุญแจ (variable Boolean ชื่อ bHasKey บนตัว player) หรือ print string "Locked!" ถ้าไม่มี การกด E ตอนไม่ได้อยู่ใกล้ประตูควรไม่มีอะไรเกิดขึ้น อธิบาย (เป็นคำพูดหรือ ASCII diagram แบบที่ใช้ในหัวข้อนี้) ว่าต้องใช้ Event node ไหน และ Branch logic ควรต่อสายยังไงevent InputAction เพียงอย่างเดียวบอกไม่ได้ว่า player อยู่ใกล้ประตูหรือเปล่า — การกดปุ่มเป็น global graph จึงต้องมี Boolean flag ที่ track ว่า "player อยู่ในระยะตอนนี้ไหม" ตั้งค่าโดย overlap event ของประตูเอง แล้วค่อยเช็คตอนกดปุ่ม
มี Event node แยกกันสามอันป้อนเข้า graph นี้: overlap event สองอันแค่คอย maintain flag bInRange ไม่ทำอะไรอย่างอื่น ส่วน input event เป็นคนทำงานจริง แต่ทำหลังเช็ค bInRange ก่อน แล้วค่อยเช็ค bHasKey ที่ซ้อนอยู่ข้างใน Branch สองอันต่อกันแบบนี้ทำงานเหมือน if (bInRange) { if (bHasKey) { ... } else { ... } } ใน C++
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
(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 ขึ้นอยู่กับว่าควรผูกกันแน่นแค่ไหน