มีการออกแบบอะไรบ้างสำหรับระบบเอนทิตีที่อิงส่วนประกอบที่ใช้งานง่าย แต่ยังยืดหยุ่นได้


23

ฉันได้รับความสนใจในระบบเอนทิตีที่อิงองค์ประกอบอยู่พักหนึ่งแล้วอ่านบทความที่นับไม่ถ้วนในเกม (เกม InsomiacมาตรฐานEvolve Your Hierarchy , T-Machine , Chronoclast ... เพื่อชื่อไม่กี่คน)

พวกเขาดูเหมือนจะมีโครงสร้างด้านนอกของสิ่งที่ชอบ:

Entity e = Entity.Create();
e.AddComponent(RenderComponent, ...);
//do lots of stuff
e.GetComponent<PositionComponent>(...).SetPos(4, 5, 6);

และถ้าคุณนำความคิดของข้อมูลที่ใช้ร่วมกัน (นี่คือการออกแบบที่ดีที่สุดที่ฉันเคยเห็นมาในแง่ของการไม่ทำซ้ำข้อมูลทุกที่)

e.GetProperty<string>("Name").Value = "blah";

ใช่มันมีประสิทธิภาพมาก อย่างไรก็ตามการอ่านหรือเขียนนั้นไม่ใช่วิธีที่ง่ายที่สุด มันรู้สึกว่าเป็น clunky และทำงานกับคุณ

โดยส่วนตัวฉันต้องการทำสิ่งที่ชอบ:

e.SetPosition(4, 5, 6);
e.Name = "Blah";

แม้ว่าแน่นอนว่าหนทางเดียวที่จะได้รับการออกแบบนั้นกลับมาอยู่ในเอนทิตี -> NPC-> ศัตรู -> FlyingEnemy-> FlyingEnemy- ด้วย FlyingAnemy ด้วยลำดับขั้นแบบที่การออกแบบนี้พยายามหลีกเลี่ยง

มีใครเคยเห็นการออกแบบสำหรับระบบส่วนประกอบชนิดนี้ซึ่งยังคงความยืดหยุ่น แต่ยังคงความเป็นมิตรกับผู้ใช้อยู่ในระดับ? และสำหรับเรื่องนั้นจัดการที่จะหลีกเลี่ยงการจัดเก็บข้อมูล (อาจเป็นปัญหาที่ยากที่สุด) ในทางที่ดี?

มีการออกแบบอะไรบ้างสำหรับระบบเอนทิตีที่อิงส่วนประกอบที่ใช้งานง่าย แต่ยังยืดหยุ่นได้

คำตอบ:


13

หนึ่งในสิ่งที่ Unity ทำคือให้สิทธิ์ผู้ช่วยผู้ช่วยบนวัตถุเกมแม่เพื่อให้ผู้ใช้เข้าถึงองค์ประกอบทั่วไปได้ง่ายขึ้น

ตัวอย่างเช่นคุณอาจมีตำแหน่งของคุณเก็บไว้ในองค์ประกอบการแปลง โดยใช้ตัวอย่างของคุณคุณจะต้องเขียนสิ่งที่ชอบ

e.GetComponent<Transform>().position = new Vector3( whatever );

แต่ใน Unity นั้นจะง่ายขึ้น

e.transform.position = ....;

ในกรณีที่transformเป็นตัวอักษรเพียงวิธีการที่ง่ายผู้ช่วยในฐานGameObjectชั้น ( Entityชั้นในกรณีของคุณ) ที่ไม่

Transform transform
{ 
    get { return this.GetComponent<Transform>(); }
}

ความสามัคคียังทำสิ่งอื่น ๆ อีกสองสามอย่างเช่นการตั้งค่าคุณสมบัติ "ชื่อ" บนวัตถุของเกมแทนที่จะเป็นองค์ประกอบย่อย

โดยส่วนตัวแล้วฉันไม่ชอบแนวคิดของการออกแบบที่แชร์ข้อมูลด้วยชื่อของคุณ การเข้าถึงคุณสมบัติตามชื่อแทนที่จะเป็นตัวแปรและการมีผู้ใช้ยังต้องทราบด้วยว่ามันเป็นประเภทใด สิ่งที่ Unity ทำคือพวกเขาใช้วิธีการที่คล้ายกันเป็นGameObject transformคุณสมบัติภายในComponentคลาสเพื่อเข้าถึงคอมโพเนนต์ sibling ดังนั้นองค์ประกอบใด ๆ ที่คุณเขียนสามารถทำสิ่งนี้:

var myPos = this.transform.position;

ในการเข้าถึงตำแหน่ง สถานที่transformให้บริการทำอะไรเช่นนี้

Transform transform
{
    get { return this.gameObject.GetComponent<Transform>(); }
}

แน่นอนว่ามันเป็นเล็ก ๆ น้อย ๆมากขึ้นอย่างละเอียดมากกว่าเพียงแค่พูดe.position = whateverแต่คุณจะได้ใช้มันและมันก็ไม่ได้ดูเป็นที่น่ารังเกียจเป็นคุณสมบัติทั่วไป และใช่คุณจะต้องทำมันเป็นทางอ้อมสำหรับองค์ประกอบลูกค้าของคุณ แต่แนวคิดก็คือส่วนประกอบ "เอนจิ้น" ของคุณทั้งหมด (renderers, colliders, แหล่งเสียง, แปลง ฯลฯ ) มี accessors ที่ง่าย


ฉันสามารถดูได้ว่าเหตุใดระบบแชร์ข้อมูลชื่ออาจมีปัญหา แต่ฉันจะหลีกเลี่ยงปัญหาขององค์ประกอบหลายรายการที่ต้องการข้อมูลได้อย่างไร (เช่นฉันจะเก็บข้อมูลอะไรไว้) แค่ระวังในขั้นตอนการออกแบบเหรอ?
The Duck Communist

นอกจากนี้ในแง่ของ 'การมีผู้ใช้ยังต้องรู้ว่ามันคืออะไร' ฉันไม่สามารถส่งไปที่ property.GetType () ในเมธอด get () ได้หรือไม่
The Duck Communist

1
คุณกำลังผลักดันข้อมูลส่วนกลาง (ในขอบเขตเอนทิตี้) ชนิดใดไปยังที่เก็บข้อมูลที่ใช้ร่วมกันเหล่านี้ ข้อมูลวัตถุประเภทเกมใด ๆ ควรเข้าถึงได้ง่ายผ่านคลาส "เอ็นจิ้น" ของคุณ (การแปลง, colliders, renderers เป็นต้น) หากคุณทำการเปลี่ยนแปลงตรรกะของเกมโดยใช้ "ข้อมูลที่ใช้ร่วมกัน" นี้จะปลอดภัยกว่าที่จะมีหนึ่งคลาสเป็นของตัวเองและจริง ๆ แล้วตรวจสอบให้แน่ใจว่าส่วนประกอบนั้นอยู่บนวัตถุของเกมนั้นมากกว่าแค่ขอให้คนตาบอด ใช่คุณควรจัดการกับมันในขั้นตอนการออกแบบ คุณสามารถสร้างส่วนประกอบที่เก็บข้อมูลได้ตลอดเวลาถ้าคุณต้องการมันจริงๆ
Tetrad

@Tetrad - คุณสามารถอธิบายสั้น ๆ เกี่ยวกับวิธีที่ GetComponent <Transform> ที่เสนอของคุณทำงานอย่างไร มันจะวนลูปผ่านส่วนประกอบทั้งหมดของ GameObject และส่งคืนคอมโพเนนต์แรกของประเภท T หรือไม่ หรืออย่างอื่นเกิดขึ้นที่นั่น?
Michael

@Michael ที่จะทำงาน คุณสามารถทำให้ผู้เรียกต้องแคชว่าเป็นการปรับปรุงประสิทธิภาพ
Tetrad

3

ฉันอยากจะแนะนำคลาสอินเทอร์เฟซสำหรับวัตถุ Entity ของคุณน่าจะดี มันสามารถทำการจัดการข้อผิดพลาดด้วยการตรวจสอบเพื่อให้แน่ใจว่าเอนทิตีประกอบด้วยส่วนประกอบของค่าที่เหมาะสมเช่นกันในที่เดียวดังนั้นคุณจะไม่ต้องทำทุกที่ที่คุณเข้าถึงค่า อีกวิธีหนึ่งการออกแบบส่วนใหญ่ที่ฉันเคยทำกับระบบที่ใช้องค์ประกอบฉันจัดการกับส่วนประกอบโดยตรงขอตัวอย่างเช่นองค์ประกอบตำแหน่งของพวกเขาแล้วเข้าถึง / ปรับปรุงคุณสมบัติขององค์ประกอบนั้นโดยตรง

พื้นฐานมากในระดับสูงคลาสใช้ในเอนทิตีที่เป็นปัญหาและจัดเตรียมอินเทอร์เฟซที่ง่ายต่อการใช้งานกับชิ้นส่วนส่วนประกอบพื้นฐานดังนี้:

public class EntityInterface
{
    private Entity entity { get; set };
    public EntityInterface(Entity e)
    {
        entity = e;
    }

    public Vector3 Position
    {
        get
        {
            return entity.GetProperty<Vector3>("Position");
        }
        set
        {
            entity.GetProperty<Vector3>("Position") = value;
        }
    }
}

ถ้าฉันคิดว่าฉันต้องจัดการกับ NoPositionComponentException หรืออะไรบางอย่างข้อดีของการทำเช่นนี้คือเพียงแค่นำทั้งหมดนั้นไปไว้ในคลาส Entity
Duck คอมมิวนิสต์

ไม่แน่ใจว่าสิ่งที่คลาสเอนทิตี้ของคุณมีอยู่จริงฉันไม่สามารถพูดได้ว่ามีข้อได้เปรียบหรือไม่ ฉันเองสนับสนุนระบบส่วนประกอบที่ไม่มีเอนทิตีฐานต่อการพูดเพียงเพื่อหลีกเลี่ยงคำถามที่ว่า 'มีอะไรอยู่ในนั้น' อย่างไรก็ตามฉันจะพิจารณาคลาสอินเตอร์เฟสเช่นนี้เป็นอาร์กิวเมนต์ที่ดีสำหรับมัน
James

ฉันเห็นว่ามันง่ายกว่านี้ (ฉันไม่ต้องค้นหาตำแหน่งผ่าน GetProperty) แต่ฉันไม่เห็นข้อดีของวิธีนี้แทนที่จะวางไว้ในคลาส Entity
The Duck คอมมิวนิสต์

2

ใน Python คุณสามารถดักจับส่วน 'setPosition' ของe.SetPosition(4,5,6)ผ่านการประกาศ__getattr__ฟังก์ชันบน Entity ฟังก์ชั่นนี้สามารถวนซ้ำผ่านส่วนประกอบต่างๆและค้นหาวิธีการหรือคุณสมบัติที่เหมาะสมและคืนค่านั้นเพื่อให้การเรียกใช้ฟังก์ชันหรือการมอบหมายไปยังตำแหน่งที่ถูกต้อง บางที C # มีระบบที่คล้ายกัน แต่อาจเป็นไปไม่ได้เนื่องจากมันถูกพิมพ์แบบคงที่ - มันอาจไม่สามารถรวบรวมได้e.setPositionเว้นแต่ e จะมี setPosition ในส่วนติดต่อ

คุณอาจทำให้เอนทิตีทั้งหมดของคุณใช้อินเทอร์เฟซที่เกี่ยวข้องสำหรับส่วนประกอบทั้งหมดที่คุณอาจเคยเพิ่มเข้าไปด้วยด้วยวิธีการสตับที่เพิ่มข้อยกเว้น จากนั้นเมื่อคุณเรียกใช้ addComponent คุณเพียงแค่เปลี่ยนฟังก์ชั่นของเอนทิตีสำหรับอินเทอร์เฟซขององค์ประกอบนั้นไปยังส่วนประกอบ แม้ว่าบิตเที่ยวยุ่งยิ่ง

แต่บางทีอาจจะง่ายที่สุดจะเกินผู้ประกอบการ [] บนชั้น Entity e["Name"] = "blah"ของคุณเพื่อค้นหาส่วนประกอบที่แนบมาสำหรับการปรากฏตัวของสถานที่ให้บริการที่ถูกต้องและกลับว่าเพื่อที่จะสามารถได้รับมอบหมายให้ชอบดังนั้น: แต่ละองค์ประกอบจะต้องใช้ GetPropertyByName ของตนเองและ Entity เรียกแต่ละรายการจนกว่าจะพบส่วนประกอบที่รับผิดชอบคุณสมบัติที่เป็นปัญหา


ฉันคิดว่าจะใช้ตัวจัดทำดัชนี .. มันดีจริงๆ ฉันจะรอสักครู่เพื่อดูว่ามีอะไรปรากฏขึ้นอีกบ้าง แต่ฉันกลับไปรวมสิ่งนี้ GetComponent () ตามปกติและคุณสมบัติบางอย่างเช่น @James ที่แนะนำสำหรับส่วนประกอบทั่วไป
The Duck คอมมิวนิสต์

ฉันอาจจะพลาดสิ่งที่กำลังพูดอยู่ที่นี่ แต่สิ่งนี้ดูเหมือนจะเป็นการทดแทนสำหรับ e.GetProperty <type> ("NameOfProperty") ไม่ว่าจะด้วย e ["NameOfProperty"] อะไรก็ตาม ??
James

@James มันเป็น wrapper สำหรับมัน (คล้ายกับการออกแบบของคุณ) .. ยกเว้นมันเป็นแบบไดนามิกมากขึ้น - มันทำโดยอัตโนมัติและสะอาดกว่าวิธีการGetProperty.. ข้อเสียเพียงอย่างเดียวคือการจัดการสตริงและการเปรียบเทียบ แต่นั่นเป็นจุดที่สงสัย
The Duck คอมมิวนิสต์

อาฉันเข้าใจผิดตรงนั้น ฉันถือว่า GetProperty เป็นส่วนหนึ่งของเอนทิตี (และจะทำสิ่งที่อธิบายไว้ข้างต้น) ไม่ใช่วิธี C #
James

2

เพื่อขยายคำตอบของ Kylotan ถ้าคุณกำลังใช้ C # 4.0 คุณสามารถพิมพ์แบบคงที่ตัวแปรที่จะเป็นแบบไดนามิก

หากคุณสืบทอดจาก System.Dynamic.DynamicObject คุณสามารถแทนที่ TryGetMember และ TrySetMember (ในวิธีการเสมือนจำนวนมาก) เพื่อดักจับชื่อคอมโพเนนต์และส่งคืนคอมโพเนนต์ที่ร้องขอ

dynamic entity = EntityRegistry.Entities[1000];
entity.MyComponent.MyValue = 100;

ฉันเขียนเกี่ยวกับระบบเอนทิตีในช่วงหลายปีที่ผ่านมา แต่ฉันคิดว่าฉันไม่สามารถแข่งขันกับยักษ์ได้ หมายเหตุ ES บางตัว (ค่อนข้างล้าสมัยและไม่จำเป็นต้องสะท้อนความเข้าใจในปัจจุบันของฉันเกี่ยวกับระบบเอนทิตี แต่มันก็คุ้มค่าที่จะอ่าน imho)


ขอบคุณสำหรับสิ่งนั้นจะช่วยให้ :) วิธีแบบไดนามิกจะไม่มีผลเช่นเดียวกับการส่งไปยัง Property.GetType () ใช่ไหม?
The Duck Communist

จะมีการส่งในบางครั้งเนื่องจาก TryGetMember มีobjectพารามิเตอร์out - แต่ทั้งหมดนี้จะได้รับการแก้ไขในขณะใช้งานจริง ฉันคิดว่ามันเป็นวิธีที่น่ายกย่องในการรักษาส่วนต่อประสานอย่างคล่องแคล่วเหนือเอนทิตี IEGryComponent (compName, out component) ฉันไม่แน่ใจว่าฉันเข้าใจคำถามอย่างไร :)
เรน

คำถามคือฉันสามารถใช้ชนิดไดนามิกทุกที่หรือ (AFAICS) ที่ส่งคืนคุณสมบัติ (property.GetType ())
The Duck Communist

1

ฉันเห็นความสนใจมากมายที่นี่ใน ES ใน C # ลองดูพอร์ตของฉันเกี่ยวกับการติดตั้ง ES อย่างยอดเยี่ยม Artemis:

https://github.com/thelinuxlich/artemis_CSharp

นอกจากนี้เกมตัวอย่างเพื่อให้คุณเริ่มต้นกับวิธีการทำงาน (ใช้ XNA 4): https://github.com/thelinuxlich/starwarrior_CSharp

ข้อเสนอแนะยินดีต้อนรับ!


ดูดีอย่างแน่นอน ฉันไม่ใช่แฟนตัวยงของแนวคิดของ EntityProcessingSystems จำนวนมาก - ในโค้ดมันมีวิธีการทำงานอย่างไรในแง่ของการใช้งาน?
คอมมิวนิสต์ Duck

ทุกระบบเป็นมุมมองที่ประมวลผลส่วนประกอบที่พึ่งพา ดูสิ่งนี้เพื่อรับแนวคิด: github.com/thelinuxlich/starwarrior_CSharp/tree/master/…
thelinuxlich

0

ฉันตัดสินใจใช้ระบบการสะท้อนที่ยอดเยี่ยมของ C # ฉันใช้มันออกจากระบบส่วนประกอบทั่วไปรวมทั้งคำตอบผสมที่นี่

มีการเพิ่มส่วนประกอบอย่างง่ายดาย:

Entity e = new Entity(); //No templates/archetypes yet.
e.AddComponent(new HealthComponent(100));

และสามารถสอบถามได้ทั้งตามชื่อประเภทหรือ (ถ้าคนทั่วไปเช่นสุขภาพและตำแหน่ง) โดยคุณสมบัติ:

e["Health"] //This, unfortunately, is a bit slower since it requires a dict search
e.GetComponent<HealthComponent>()
e.Health

และลบออก:

e.RemoveComponent<HealthComponent>();

ข้อมูลเข้าถึงได้โดยทั่วไปโดยคุณสมบัติอีกครั้ง:

e.MaxHP

ซึ่งถูกเก็บไว้ด้านองค์ประกอบ; ค่าใช้จ่ายน้อยลงหากไม่มี HP:

public int? MaxHP
{
get
{

    return this.Health.MaxHP; //Error handling returns null if health isn't here. Excluded for clarity
}
set
{
this.Health.MaxHP = value;
}

การพิมพ์จำนวนมากได้รับความช่วยเหลือจาก Intellisense ใน VS แต่ส่วนใหญ่มีการพิมพ์เพียงเล็กน้อย - สิ่งที่ฉันกำลังมองหา

เบื้องหลังฉันกำลังเก็บDictionary<Type, Component>และComponentsแทนที่ToStringวิธีการตรวจสอบชื่อ การออกแบบดั้งเดิมของฉันใช้ชื่อและสิ่งต่าง ๆ เป็นหลัก แต่มันก็กลายเป็นหมูที่จะทำงานด้วย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.