Godot Devlog#2 - การสื่อสารกันระหว่าง node ของ Godot | muitsfriday.dev

web-logo-doge muitsfriday.dev

Godot Devlog#2 - การสื่อสารกันระหว่าง node ของ Godot

วิธีเชื่อมต่อพฤติกรรมของ node เข้าด้วยกันเพื่อร้อยเรียงให้แต่ละ node ทำงานร่วมกันเป็นส่วนประกอบที่ใหญ่ขึ้น

Wed Oct 18 2023

จากที่เรารู้ว่าการสร้างเกมใน Godot นั้นสร้างจากการเอา node ที่มีหน้าที่ที่ต้องการมาประกอบร่างกัน หลังจากนั้นเราต้องทำให้ node เหล่านั้นสื่อสารผ่านกันได้เพื่อทำงานร่วมกันได้

Godot มีกลไกที่ออกแบบมาเพื่อให้ node สามารถติดต่อกันได้ผ่านทางสิ่งที่เรียกว่า signal

Signal

Godot ออกแบบวิธีติดต่อกันระหว่าง node ด้วย signal(สัญญาณ) เมื่อเกิดเหตุการณ์จำเพาะบางอย่าง node สามารถ emit เหตุการณ์บางอย่างออกมาได้เพื่อให้ไครก็ตามที่สนใจรับมือกับเหตุการณ์นั้นๆ ตามสมควร ถ้าให้เปรียบเทียบคงจะคล้ายๆ event listener ของการเขียนโปรแกรมทั่วไป

ในแต่ละ node ที่ Godot มีมาให้จะแถม signal ที่ node นั้นใช้มาให้ด้วย แต่นักพัฒนาเองสามารถสร้าง signal ขึ้นมาเองได้เช่นกัน

และเช่นเดียวกัน แต่ละ node ของ Godot ก็สามารถรอรับฟัง signal จาก node อื่นๆเพื่อทำงานบางอย่างเมื่อ signal นั้นถูกส่งออกมาเรียกว่าการ connect

Godot connect signal

ปัญหา

สุดท้ายโครงสร้างของเกมที่สร้างด้วย Godot จะกลายเป็น tree ของ node ปริมาณมาก การเชื่อมต่อระหว่าง node จะกลายเป็นสิ่งยุ่งยากหากเราต้องไล่หาว่า node ที่เราต้องการ connect signal อยู่ตรงไหน

ยกตัวอย่างเช่นเรามี Player อยู่ใน world scene และก็มี inventory เราอยากให้ Player ทำการรับมือบางอย่างเมื่อ inventory มีการเปลี่ยนแปลง

signal connect godot

ปัญหาอยู่ที่ว่า player และ inventory เป็น scene(node) ที่อยู่แยกกันต่างหาก ซึ่ง player ไม่มีทางรู้ได้ว่าจริงๆแล้ว inventory ถูกแปะอยู่ตรงไหนของ tree

การเข้าถึง node จากจุดเริ่มต้นเราสามารถ traverse ได้ในแบบ tree ปกติคือเข้าถึง node ลูก(child)หรือพ่อแม่(parent) แต่ในกรณีส่วนมาก เรามักจะไม่ค่อยรู้(และไม่ได้อยากรู้ด้วย)ว่า signal ถูก emit มาจากส่วนไหนของ tree เราสนแค่ว่า event นั้นจะเกิดขึ้นตอนไหนซึ่งจุดนี้เรามีวิธีแก้ไข

Eventbus และ Autoloading Class

มี Pattern ที่เรามักจะใช้จัดการกรณีนี้คือ Eventbus

เมื่อเราไม่ได้สนใจว่าไครเป็นคนรับ-ส่ง signal เราก็จะทำก้อน class ตรงกลางลอยๆเอาไว้เป็นตัวกลางจัดการเรื่องนี้เรียก class นี้ว่า Eventbus

Node ที่ต้องการส่ง signal จะไม่ emit ออกไปเองแต่จะไปขอให้ Eventbus เป็นคน emit ให้

Node ที่ต้องการรับ signal ไม่ต้องการหาผู้ส่งที่แท้จริง แต่จะไป connect ผ่าน Eventbus ที่เป็นตัวกลางแทนเพื่อรอรับ signal ที่สนใจ

signal connect godot

แบบนี้การรับส่ง signal จะมีขึ้นอยู่กับตำแหน่งใน tree ของ node ที่เกี่ยวข้อง สามารถย้ายไปมาอิสระโดยไม่ต้องกังวลเรื่องการ connect event เพราะ Eventbus ไม่ได้อยู่ใน tree แต่ลอยอยู่เหนือมันอีกที เป็น class กลางที่ควรจะเข้าถึงได้จากทุกที่

Godot มีวิธีสร้าง global class เรียกว่าการทำ class autoloading เป็นการเอา script ไป register ไว้กับตัว engine เพื่อบอกว่าเดี๋ยวตอนเริ่มรันเกมให้ start class นี้ขึ้นมาและให้ class นี้สามารถเข้าถึงได้จากทุกที่

signal connect autoloading class

Engine signal

Node บางประเภทมีการ emit signal ออกมาเองเมื่อตรงตามเงื่อนไข โดยเราไม่ต้องกำหนดขึ้นมาเองเช่น หากเรามี Area2D สอง node เมื่อมีการทับซ้อนกันของ Area2D เกิดขึ้นหากเราไปดูใน Area2D ว่ามี signal อะไรบ้างจะเห็นว่ามี

Area2D signal

area_entered ถูก emit ก็ต่อเมื่อมี Area2D มาชนกับมันโดยจะส่งข้อมูล Area2D ที่ชนแนบมาให้ด้วย เรามักจะใช้ประโยชน์จากตรงนี้ทำสคริปที่ช่วยให้เกิดพฤติกรรมที่ต้องการได้เช่น

หากเรามี scene ItemDrop ที่ใช้แสดงถึง item ที่หล่นในแผนที่ เราอยากให้ player สามารถมาเก็บ item นี้ได้เราจะสร้าง Area2D ขึ้นมาให้ scene นี้เพื่อรอรับว่าหากมี Area2D เข้ามาทับในพื้นที่จะรัน script เพื่อให้ player เก็บของ

Aread2D godot

จากนั้นเรา connect area_enterd signal ผ่าน script ที่แปะไว้ใน Itemdrop script เพื่อเขียนโค้ดให้รับมือกับการดึง item ชิ้นนี้เข้าตัว player

Aread2D godot signal connect

Area2D จะถูกหยิบขึ้นมาใช้ในเกมบ่อยมาก เพราะเรามักอยากเช็กเรื่องการเข้าออกพื้นที่เสมอ เช่น

  • เข้าออกประตูเพือเปลี่ยน map(scene)
  • พื้นที่ตตรวจจับของศัตรู เพื่อเปลี่ยน state ให้ศัตรูหันมาตามโจมตีเรา
  • พื้นที่โดนดาเมจ(hitbox) ตรวจจับกระสุนหรืออาวุท

เลือกใช้ยังไง

Node/Scene ควรทำงานอยู่ได้ด้วยตัวเองเพื่อตอบรับจุดประสงค์บางอย่าง โดยพยายามมองว่ามันสามารถเอาไปใช้ที่ตรงไหนของ tree ก็ได้ดังนั้นจึงควรพยายามให้มันทำงานเล็กพอที่สุด แต่ควรทำงานได้ด้วยตัวเองโดยไม่ต้องการปัจจัยภายนอกมากเกินไป

ถ้า signal ที่ใช้เกิดจากภายในตัว node เองและใช้เพื่อจุดประสงให้ node ตัวเองทำงานได้ ภายนอกไม่ต้องรู้ เราควร connect กันเองภายใน node เช่น เดียวกับ Aread2D ใน ItemDrop ที่พื้นที่นั้นถูกใส่เข้ามาเพื่อตรวจจับผู้เล่นที่เข้ามา

แต่ถ้าจุดประสงค์คือการ expose ความสามารถให้ node/scene อื่นๆใช้ให้ทำผ่าน Bus เพื่อลดการเชื่อมต่อผ่าน tree ที่มีการเปลี่ยนแปลงได้บ่อยและซับซ้อน

Example

ตัวอย่างโค้ด Bus อันนี้คือ ActiveItemBus ทำหน้าที่รับส่ง signal เกี่ยวกับการ active item ของตัวละคร

public partial class ActiveItemBus : Node
{
    [Signal]
    public delegate void OnItemActiveEventHandler(Inventory inv, int index);

    private static ActiveItemBus _instance = null!;

    public static ActiveItemBus Instance()
    {
        return _instance;
    }

    public static void EmitOnItemActive(Inventory inv, int index)
    {
        Instance().EmitSignal(SignalName.OnItemActive, inv, index);
    }

    public override void _Ready()
    {
        base._Ready();

        _instance = this;
    }
}

OnItemActiveEventHandler คือ signal ที่อยู่ใน bus นี้กำหนดว่าหากมี signal นี้จะส่ง Inventory object ที่เก็บข้อมูลช่องเก็บของตัวละครมา และ index ที่บอกว่าช่องไหนของ inventory ที่ active

มีการใช้ singleton pattern เพราะเวลา emit/connect signal จะต้องทำผ่าน object instance ไม่สามารถทำผ่านชื่อ class (static access) ได้

EmitOnItemActive จะถูกเรียกโดย node ที่เปลี่ยน active item