เอ็นจิ้นที่ใช้ระบบเอ็นติตี้คอมโพเนนต์


9

หมายเหตุ: ฉันกำลังเขียนโปรแกรมนี้ใน Javascript แต่ควรเป็นภาษาที่ไม่เชื่อเรื่องพระเจ้าส่วนใหญ่

ฉันกำลังคิดที่จะเปลี่ยนเอนจินของฉันให้เป็น ECS

ฉันได้รับแนวคิดพื้นฐาน ( หมายเหตุ: นี่ผิดโปรดดูคำตอบของฉัน ):

หน่วยงานเป็นวัตถุเกม
คอมโพเนนต์คือบิตของฟังก์ชัน ( reactToInput()) หรือสถานะ ( position) ซึ่งสามารถรับ "ติดกาว" กับเอนทิตี
ระบบมีรายการเอนทิตีที่จัดการและอัปเดต

แต่ฉันไม่แน่ใจว่าจะได้รับการใช้งานและรายละเอียดบางอย่าง ...

คำถาม: ระบบสามารถทำงานกับเอนทิตีประเภทต่าง ๆ ได้หรือไม่? ฉันมักจะให้ตัวอย่างของคลาสที่เรียกSceneในเครื่องยนต์ของฉันและมันจะให้บริการตามวัตถุประสงค์นี้ทันทีเช่นกัน ฉากหนึ่งเป็นคอนเทนเนอร์ของวัตถุทั้งหมดที่สามารถแสดงผลอัปเดตส่งผลกระทบต่อการเรนเดอร์ (ไฟ) และบางทีในอนาคตแม้กระทั่ง2DSoundEmitterวัตถุ มีอินเทอร์เฟซระดับสูงเพื่อให้ผู้ใช้ไม่จำเป็นต้องกังวลเกี่ยวกับประเภทของวัตถุที่เขามีอยู่scene.add()และสิ่งต่าง ๆ ทั้งหมดนั้น

ฉันตระหนักว่าSceneอาจเป็นระบบ ใช้เวลาในเอนทิตีจัดเก็บและจากนั้นสามารถเรียกวิธีการอัพเดตของพวกเขาและอาจทำการเปลี่ยนแปลงสถานะบางอย่าง แต่มีปัญหา: ตามที่ฉันได้อธิบายไว้ข้างต้นแล้วมันSceneสามารถเลี้ยงวัตถุประเภทต่าง ๆ ได้ ! ฉันควรทำอย่างไรในสถานการณ์ที่มีทั้งวัตถุที่สามารถถ่ายภาพได้ ("drawable") และไฟในนั้น ฉันควรจะตรวจสอบเอนทิตีก่อนทำการโต้ตอบหรือไม่ หรือฉันควรจะแก้ปัญหาในระดับที่ต่ำกว่า: สร้างLightSourceส่วนประกอบที่สามารถเพิ่มเข้าไปในวัตถุใด ๆและแสงก็จะเป็นนิติบุคคลที่มีLightSourceและPositionส่วนประกอบ เป็นที่ยอมรับหรือไม่?

นอกจากนี้เป็นวิธีปฏิบัติที่ดีที่ยังคงใช้การสืบทอดดั้งเดิมและคลาสดั้งเดิมหรือไม่? ตัวอย่างเช่นฉันแค่คิดออกไม่ออกว่าจะRendererเป็นอะไร! ไม่ใช่ระบบเนื่องจากฟังก์ชั่นเดียวที่ใช้ในกล้องและฉากแสดงทุกอย่างและใช้เอฟเฟกต์ (เช่นเงา) นอกจากนี้ยังจัดการบริบทความกว้างและความสูงของเกมทำการแปล ... แต่มันยังไม่ใช่ระบบ!

แก้ไข:คุณสามารถลิงค์แหล่งข้อมูลใด ๆ ที่คุณพบบน ECS ได้ไหม ฉันมีปัญหาในการหาสิ่งที่ดี


2
แทนที่จะโพสต์คำตอบในหน้านี้ใหม่ฉันจะให้ลิงค์นี้: gamedev.stackexchange.com/questions/23533/… ไม่ควรรับเอนทิตีจากเอนทิตี้ของความแตกต่างระหว่างเอนทิตีควรทำผ่านส่วนประกอบ โดยทั่วไปคุณจะต้องใช้อินเทอร์เฟซสำหรับแต่ละระบบหลัก (การแสดงผล, ฟิสิกส์, ระบบเครือข่าย, อินพุต, เสียง, ฯลฯ ... ) วิธีที่ฉันตั้งค่าตัวแสดงผลของฉันคือการสอบถามซีนสำหรับเอนทิตีที่สามารถเรนเดอร์ได้และตัวจัดการฉากจะขอให้แต่ละเอนทิตีที่มีองค์ประกอบการเรนเดอร์แสดงข้อมูลการเรนเดอร์
Nic Foster

1
การออกแบบชิ้นส่วนในบล็อก T = Machine (ตั้งแต่คุณขอให้ติดอันดับที่ดี)
John McDonald

รหัสและการอภิปรายเกี่ยวกับกรอบเอนทิตี: gamadu.com/artemis
Patrick Hughes

@ JohnMcDonald ฉันเขียนความคิดเห็นในบทความนั้นแม้ว่ามันจะรอการกลั่นกรอง คุณสามารถดูได้ที่นี่: t-machine.org/index.php/2007/12/22/... ฉัน "ยานแบน"
jcora

นอกจากนี้ @NicFoster บทความที่ John เชื่อมโยงกับ T = Machine จะอธิบายบางอย่างที่แตกต่างจากคำตอบของคุณ ... เดฟเอนทิตี้ไม่มีรายการส่วนประกอบพวกมันเป็นเพียงชื่อ เช่นเดียวกับ "flsjn304" - นั่นคือเอนทิตี มันถูกเก็บไว้ "ที่ไหนสักแห่ง" และฉันต้องอ่านสิ่งที่ต้องเข้าใจอีกครั้งถ้าเขาเก็บส่วนประกอบไว้ในระบบซึ่งมันแปลกมากสำหรับฉัน!
jcora

คำตอบ:


6

ให้ฉันดูด้วยการพยายามทำความเข้าใจในฐานะเว็บ / UI JS dev ฉันสามารถช่วยได้ นอกจากนี้อย่าไปไกลเกินไปในเรื่องความไม่เชื่อเรื่องภาษา รูปแบบจำนวนมากที่จัดตั้งขึ้นในภาษาอื่น ๆ นั้นมีค่าในการเรียน แต่สามารถนำไปใช้ใน JS แตกต่างกันมากเนื่องจากมีความยืดหยุ่นหรือไม่จำเป็นจริง ๆ เนื่องจากลักษณะของภาษาที่ละเอียดอ่อน คุณสามารถสร้างโอกาสบางอย่างได้ถ้าคุณเขียนโค้ดที่คิดว่า JS มีขอบเขตชุดเดียวกันกับที่ภาษา OOP เชิงดั้งเดิมจะทำ

ก่อนอื่นบนปัจจัย "ไม่ใช้ OOP" โปรดจำไว้ว่าวัตถุ JavaScript นั้นเป็นเหมือน playdough เมื่อเทียบกับภาษาอื่นและคุณต้องออกจากวิธีการสร้างโครงร่างการสืบทอด - มรดกสืบทอดเนื่องจาก JS ไม่ใช่คลาส - เบสและคอมโพสิตมาเป็นธรรมชาติมากขึ้นกับมัน หากคุณกำลังใช้คลาส hand-me-down ที่โง่เขลาหรือระบบต้นแบบใน JS ของคุณให้ลองทำดู ใน JS เราใช้การปิดต้นแบบและเราผ่านฟังก์ชั่นรอบ ๆ เหมือนขนม มันน่าขยะแขยงและสกปรกและไม่ถูกต้อง แต่ก็มีประสิทธิภาพรัดกุมและนั่นคือวิธีที่เราชอบ

วิธีการที่สืบทอดมาอย่างหนักนั้นถูกสะกดออกมาเป็นรูปแบบต่อต้านในรูปแบบการออกแบบและด้วยเหตุผลที่ดีเช่นเดียวกับใครก็ตามที่เดินลงไป 15+ ระดับของคลาสหรือสิ่งก่อสร้างที่มีลักษณะคล้ายชั้นเรียน กำลังเข้ามาจากสามารถบอกคุณได้

ฉันไม่รู้ว่าทำไมโปรแกรมเมอร์จำนวนมากถึงชอบทำสิ่งนี้ (โดยเฉพาะอย่างยิ่งพวกจาวาที่เขียน JavaScript ด้วยเหตุผลบางอย่าง) แต่มันแย่มากอ่านไม่ออกและไม่ย่อท้ออย่างสมบูรณ์เมื่อเคยเกิน การรับมรดกไม่เป็นไรที่นี่และที่นั่น แต่ไม่จำเป็นจริงๆใน JS ในภาษาที่มีทางลัดที่น่าดึงดูดใจมากขึ้นควรสงวนไว้สำหรับข้อกังวลทางสถาปัตยกรรมที่เป็นนามธรรมมากกว่าการสร้างแบบจำลองตามตัวอักษรมากขึ้นเช่นการสร้างการใช้งานผีดิบผ่านเครือข่ายการสืบทอดที่รวม BunnyRabbit เพราะมันทำงานได้จริง นั่นไม่ใช่การนำโค้ดที่ดีมาใช้ซ้ำ มันเป็นฝันร้ายของการบำรุงรักษา

ในฐานะที่เป็นเอ็นจิ้นที่ใช้ JS dev Entity / Component / System ทำให้ฉันเป็นระบบ / รูปแบบสำหรับการแยกข้อกังวลในการออกแบบและจากนั้นรวบรวมวัตถุสำหรับการนำไปใช้ในระดับที่ละเอียดมาก กล่าวอีกนัยหนึ่งการเล่นของเด็กในภาษาเช่น JavaScript แต่ให้ฉันดูก่อนว่าฉันกำลังทำแบบนี้ถูกต้องก่อนไหม

  • เอนทิตี - สิ่งเฉพาะที่คุณกำลังออกแบบ เรากำลังพูดถึงคำนามที่เหมาะสมมากกว่า (แต่ไม่ใช่ตามจริง) ไม่ใช่ 'ฉาก' แต่ 'IntroAreaLevelOne' IntroAreaLevelOne อาจนั่งอยู่ในกล่องฉากเหตุการณ์บางอย่าง แต่เรามุ่งเน้นไปที่บางสิ่งที่แตกต่างจากสิ่งที่เกี่ยวข้องอื่น ๆ ในรหัสหน่วยงานนั้นเป็นเพียงชื่อ (หรือ ID) ที่เชื่อมโยงกับสิ่งต่าง ๆ ที่จำเป็นต้องมีการใช้งานหรือสร้างขึ้น (ส่วนประกอบ) เพื่อให้มีประโยชน์

  • ส่วนประกอบ - ประเภทของสิ่งที่องค์กรต้องการ นี่เป็นคำนามทั่วไป เช่นเดียวกับ WalkingAnimation ภายใน WalkingAnimation เราสามารถเจาะจงมากขึ้นเช่น "Shambling" (ทางเลือกที่ดีสำหรับซอมบี้และมอนสเตอร์พืช) หรือ "ChickenWalker" (ยอดเยี่ยมสำหรับประเภทหุ่นยนต์ ed-209ish แบบย้อนกลับ) หมายเหตุ: ไม่แน่ใจว่าจะแยกแยะจากการแสดงผลของโมเดล 3 มิติแบบนั้นได้อย่างไร - อาจเป็นตัวอย่างที่น่ารำคาญ แต่ฉันเป็นโปร JS มากกว่าผู้พัฒนาเกมที่มีประสบการณ์ ใน JS ฉันจะวางกลไกการแมปในกล่องเดียวกันกับส่วนประกอบ ส่วนประกอบในสิทธิ์ของพวกเขาเองนั้นมีแนวโน้มที่จะใช้ตรรกะและมีแผนงานมากขึ้นที่จะบอกระบบของคุณว่าจะต้องใช้อะไรหากจำเป็นต้องใช้ระบบ (ในความพยายามของฉันที่ ECS ส่วนประกอบบางอย่างเป็นเพียงชุดของชุดคุณสมบัติ) เมื่อส่วนประกอบถูกสร้างขึ้นมัน '

  • ระบบ - เนื้อโปรแกรมที่แท้จริงอยู่ที่นี่ ระบบ AI ถูกสร้างขึ้นและเชื่อมโยงกันการแสดงผลสำเร็จสามารถสร้างลำดับภาพเคลื่อนไหวได้ ฯลฯ ฉันกำลังตัดออกและปล่อยสิ่งเหล่านี้ให้เป็นส่วนหนึ่งของจินตนาการ แต่ในตัวอย่าง System.AI ใช้คุณสมบัติมากมายและแยกหน้าที่ ถูกใช้เพื่อเพิ่มตัวจัดการเหตุการณ์ไปยังวัตถุที่ในที่สุดได้รับใช้ในการใช้งาน สิ่งสำคัญเกี่ยวกับ System.AI คือมันครอบคลุมองค์ประกอบหลายประเภท คุณสามารถแยกแยะเนื้อหา AI ทั้งหมดด้วยองค์ประกอบเดียว แต่การทำเช่นนั้นคือการเข้าใจผิดว่าเป็นเรื่องละเอียด

คำนึงถึงเป้าหมาย: เราต้องการทำให้ง่ายต่อการเชื่อมต่ออินเทอร์เฟซ GUI บางประเภทสำหรับผู้ที่ไม่ใช่นักออกแบบเพื่อปรับแต่งสิ่งต่าง ๆ ได้อย่างง่ายดายโดยการเพิ่มและการจับคู่ส่วนประกอบภายในกระบวนทัศน์ที่เหมาะสมกับพวกเขาและเราต้องการหลีกเลี่ยง รูปแบบโค้ดที่เป็นที่นิยมซึ่งง่ายต่อการเขียนมากกว่าที่จะแก้ไขหรือบำรุงรักษา

ดังนั้นใน JS อาจจะเป็นแบบนี้ ผู้พัฒนาเกมโปรดบอกฉันว่าฉันทำผิดไปอย่างน่ากลัว:

//I'm going with simple objects of flags over arrays of component names
//easier to read and can provide an opt-out default
//Assume a genre-bending stealth assassin game

//new (function etc... is a lazy way to define a constructor and auto-instantiate
var npcEntities = new (function NpcEntities(){

    //note: {} in JS is an object literal, a simple obj namespace (a dictionary)
    //plain ol' internal var in JS is akin to a private member
    var default={ //most NPCs are humanoids and critters - why repeat things?
        speedAttributes:true,
        maneuverAttributes:true,
        combatAttributes:true,
        walkingAnimation:true,
        runningAnimation:true,
        combatAnimation:true,
        aiOblivious:true,
        aiAggro:true,
        aiWary:true, //"I heard something!"
        aiFearful:true
    };

    //this. exposes as public

    this.zombie={ //zombies are slow, but keep on coming so don't need these
        runningAnimation:false,
        aiFearful:false
    };

    this.laserTurret={ //most defaults are pointless so ignore 'em
        ignoreDefault:true,
        combatAttributes:true,
        maneuverAttrubtes:true, //turning speed only
    };
    //also this.nerd, this.lawyer and on and on...

    //loop runs on instantiation which we're forcing on the spot

    //note: it would be silly to repeat this loop in other entity collections
    //but I'm spelling it out to keep things straight-forward.
    //Probably a good example of a place where one-level inheritance from
    //a more general entity class might make sense with hurting the pattern.
    //In JS, of course, that would be completely unnecessary. I'd just build a
    //constructor factory with a looping function new objects could access via
    //closure.

    for(var x in npcEntities){

        var thisEntity = npcEntities[x];

        if(!thisEntity.ignoreDefaults){

            thisEntity = someObjectXCopyFunction(defaults,thisEntity);
            //copies entity properties over defaults

        }
        else {
            //remove nonComponent property since we loop again later
            delete thisEntity.ignoreDefaults;
        }
    }
})() //end of entity instantiation

var npcComponents = {
    //all components should have public entityMap properties

    //No systems in use here. Just bundles of related attributes
    speedAttributes: new (function SpeedAttributes(){
        var shamblingBiped = {
            walkingAcceleration:1,
            topWalking:3
        },
        averageMan = {
            walkingAcceleration:3,
            runningAcceleration:4,
            topWalking: 4,
            topRunning: 6
        },
        programmer = {
            walkingAcceleration:1,
            runningAcceleration:100,
            topWalking:2
            topRunning:2000
        }; //end local/private vars

        //left is entity names | right is the component subcategory
        this.entityMap={
            zombie:shamblingBiped,
            lawyer:averageMan,
            nerd:programmer,
            gCostanza:programmer //makes a cameo during the fire-in-nursery stage
        }
    })(), //end speedAttributes

    //Now an example of an AI component - maps to function used to set eventHandlers
    //functions which, because JS is awesome we can pass around like candy
    //I'll just use some imaginary systems on this one

    aiFearful: new (function AiFearful(){
        var averageMan = Systems.AI({ //builds and returns eventSetting function
            fearThreshold:70, //%hitpoints remaining
            fleeFrom:'lastAttacker',
            tactic:'avoidIntercept',
            hazardAwareness:'distracted'
        }),
        programmer = Systems.AI({
            fearThreshold:95,
            fleeFrom:'anythingMoving',
            tactic:'beeline',
            hazardAwareness:'pantsCrappingPanic'
        });//end local vars/private members


         this.entityMap={
            lawyer:averageMan,
            nerd:averageMan, //nerds can run like programmers but are less cowardly
            gCostanza:programmer //makes a cameo during the fire-in-nursery stage
        }
    })(),//and more components...

    //Systems.AI is general and would get called for all the AI components.
    //It basically spits out functions used to set events on NPC objects that
    //determine their behavior. You could do it all in one shot but
    //the idea is to keep it granular enough for designers to actually tweak stuff
    //easily without tugging on developer pantlegs constantly.
    //e.g. SuperZombies, zombies, but slightly tougher, faster, smarter
}//end npcComponents

function createNPCConstructor(npcType){

    var components = npcEntities[npcType],

    //objConstructor is returned but components is still accessible via closure.

    objConstructor = function(){
        for(var x in components){
            //object iteration <property> in <object>

            var thisComponent = components[x];

            if(typeof thisComponent === 'function'){
                thisComponent.apply(this);
                //fires function as if it were a property of instance
                //would allow the function to add additional properties and set
                //event handlers via the 'this' keyword
            }
            else {
                objConstructor.prototype[x] = thisComponent;
                //public property accessed via reference to constructor prototype
                //good for low memory footprint among other things
            }
        }
    }
    return objConstructor;
}

var npcBuilders= {}; //empty object literal
for (var x in npcEntities){
    npcConstructors[x] = createNPCConstructor(x);
}

ตอนนี้เมื่อใดก็ตามที่คุณต้องการ NPC คุณสามารถสร้างได้ด้วย npcBuilders.<npcName>();

GUI สามารถเชื่อมต่อไปยังวัตถุ npcEntities และส่วนประกอบและอนุญาตให้นักออกแบบปรับแต่งเอนทิตีเก่าหรือสร้างเอนทิตีใหม่โดยเพียงแค่ผสมและจับคู่ส่วนประกอบ (แม้ว่าจะไม่มีกลไกในส่วนประกอบที่ไม่ใช่ค่าเริ่มต้น แต่ส่วนประกอบพิเศษสามารถเพิ่มได้ทันที รหัสตราบใดที่มีองค์ประกอบที่กำหนดไว้สำหรับมัน


ดูหกปีต่อมาฉันไม่แน่ใจว่าฉันเข้าใจคำตอบของฉันเอง สิ่งนี้จะได้รับการปรับปรุงหรือไม่
Erik Reppen

1

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

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

ฉันเรียนรู้มากพอที่จะครอบคลุมคำถามส่วนใหญ่ของฉันที่นี่ดังนั้นฉันจะตอบมัน

Sceneระดับที่ผมพูดถึงไม่ควรจะเป็นระบบ อย่างไรก็ตามควรเป็นผู้จัดการกลางที่สามารถเก็บเอนทิตีทั้งหมดอำนวยความสะดวกข้อความและอาจจัดการระบบ มันยังสามารถทำหน้าที่เป็นโรงงานจัดเรียงสำหรับเอนทิตีและฉันตัดสินใจที่จะใช้มันเช่นนั้น มันสามารถใช้เอนทิตี้ประเภทใดก็ได้ แต่จากนั้นจะต้องฟีดเอนทิตีนั้นไปยังระบบที่เหมาะสม (ซึ่งสำหรับเหตุผลด้านประสิทธิภาพไม่ควรทำการตรวจสอบประเภทใด ๆ เว้นแต่ว่าพวกเขาอยู่ในระดับบิต)

ฉันไม่ควรใช้ OOP ใด ๆ ในขณะที่ติดตั้ง ES แนะนำให้ Adam แต่ฉันคิดว่าไม่มีเหตุผลที่จะไม่มีอ็อบเจกต์ที่มีวิธีการสำหรับทั้งเอนทิตีและส่วนประกอบ

Rendererก็สามารถจะนำมาใช้เป็นระบบ มันจะรักษารายการของวัตถุที่สามารถวาดได้และเรียกdraw()วิธีการแสดงผลองค์ประกอบของพวกเขาในแต่ละ 16ms


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

@dreta ตัวแสดงผลในปัจจุบัน (การปรับใช้เครื่องมือที่ไม่ใช่ ES) จะทำการเปลี่ยนแปลงการเปลี่ยนแปลงของกล้องสิ่งที่เป็นอัลฟาและในอนาคตมันจะวาดเอฟเฟกต์ต่างๆ GUI และเงา ดูเหมือนเป็นเรื่องธรรมดาที่จะจัดกลุ่มสิ่งนั้น ฉากนั้นไม่ควรรับผิดชอบในการสร้างเอนทิตีที่จัดเก็บ? หรืออย่างอื่นควรเก็บพวกเขา? ส่วนการสร้างอาจเป็นเพียงไม่กี่บรรทัดของการรวมส่วนประกอบที่ผู้ใช้ให้มาด้วยกันไม่ใช่การ "สร้าง" สิ่งใด ๆ เลยเพียงแค่เริ่มต้น
jcora

ไม่ใช่ทุกสิ่งที่แสดงผลได้วัตถุทุกชิ้นไม่สามารถชนหรือเปล่งเสียงได้ด้วยวัตถุฉากของคุณคุณกำลังมีเพศสัมพันธ์มากทำไม? นี่เป็นเพียงความเจ็บปวดในการเขียนและดีบัก หน่วยงานที่ใช้ในการระบุวัตถุส่วนประกอบเก็บข้อมูลและระบบการทำงานกับข้อมูล เหตุใดคุณจึงรวมทั้งหมดเข้าด้วยกันแทนที่จะมีระบบที่เหมาะสมเช่น RenderingSystem และ SoundSystem และรบกวนระบบเหล่านั้นหากนิติบุคคลมีส่วนประกอบที่จำเป็นทั้งหมด
dreta

1
การส่งเงาจะติดอยู่กับแหล่งกำเนิดแสงเป็นปกติแม้ว่าคุณจะสามารถสร้างองค์ประกอบ "CastsShadow" และมองหาเมื่อแสดงวัตถุแบบไดนามิก หากคุณกำลังทำ 2D นี่เป็นเพียงปัญหาพื้นฐานของการสั่งซื้ออัลกอริทึมของจิตรกรธรรมดาจะแก้ปัญหานี้ให้คุณ TBH คุณกังวลเร็วเกินไป คุณจะคิดออกเมื่อถึงเวลาที่จะทำมันและคุณมีเพียงในใจของคุณตอนนี้คุณเพิ่งสับสนตัวเอง คุณไม่สามารถหวังที่จะทำให้ทุกอย่างถูกต้องในครั้งแรกมันจะไม่เกิดขึ้น คุณจะข้ามสะพานนั้นเมื่อคุณไปถึง
dreta

1
"เอนทิตีและส่วนประกอบเป็นเพียงตัวยึดข้อมูลที่โง่ขณะที่ระบบมอบการทำงานทั้งหมด" ไม่จำเป็น. พวกเขาอยู่ในแนวทางของบางคน แต่ไม่ใช่คนอื่น ดูเอ็นจิ้น Unity - พฤติกรรมทั้งหมดอยู่ในส่วนประกอบ
Kylotan

-2

การจัดการการพึ่งพาเบื้องต้นเบื้องต้น 101.

หลักสูตรนี้จะถือว่าคุณมีความรู้พื้นฐานเกี่ยวกับการฉีดการพึ่งพาและการออกแบบพื้นที่เก็บข้อมูล

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

มันเป็นไปตามวลี: " newis กาว"

ฉันจะสาธิตสิ่งนี้ใน C #

public interface IEntity
{
    int[] Position { get; }
    int[] Size { get; }
    bool Update();
    void Render();
}

public interface IRenderSystem
{
    void Draw(IEntity entity);
}

public interface IMovementSystem
{
    bool CanMoveLeft();
    void MoveLeft();
    bool CanMoveRight();
    void MoveRight();
    bool CanMoveUp();
    void MoveUp();
    bool CanMoveDown();
    void MoveDown();
    bool Moved();
    int[] Position { get; set; }
}

public interface IInputSystem
{
    string Direction { get; set; }
}

public class Player : IEntity
{
    private readonly IInputSystem _inputSystem;
    private readonly IMovementSystem _movementSystem;
    private readonly IRenderSystem _renderSystem;
    private readonly int[] _size = new[] { 10, 10 };

    public Player(IRenderSystem renderSystem, IMovementSystem movementSystem, IInputSystem inputSystem)
    {
        _renderSystem = renderSystem;
        _movementSystem = movementSystem;
        _inputSystem = inputSystem;
    }

    public bool Update()
    {
        if (_inputSystem.Direction == "Left" && _movementSystem.CanMoveLeft())
            _movementSystem.MoveLeft();
        if (_inputSystem.Direction == "Right" && _movementSystem.CanMoveRight())
            _movementSystem.MoveRight();
        if (_inputSystem.Direction == "Up" && _movementSystem.CanMoveUp())
            _movementSystem.MoveUp();
        if (_inputSystem.Direction == "Down" && _movementSystem.CanMoveDown())
            _movementSystem.MoveDown();

        return _movementSystem.Moved();
    }

    public void Render()
    {
        if (_movementSystem.Moved())
            _renderSystem.Draw(this);
    }

    public int[] Position
    {
        get { return _movementSystem.Position; }
    }

    public int[] Size
    {
        get { return _size; }
    }
}

นี่เป็นระบบพื้นฐานสำหรับการป้อนข้อมูลการเคลื่อนไหวและการแสดงผล Playerระดับเป็นนิติบุคคลในกรณีนี้และส่วนประกอบที่มีอินเตอร์เฟส Playerชั้นรู้อะไรเกี่ยวกับ internals ของวิธีการที่เป็นรูปธรรมIRenderSystem, IMovementSystemหรือIInputSystemการทำงาน อย่างไรก็ตามอินเทอร์เฟซมีวิธีการPlayerส่งสัญญาณ (เช่นเรียกใช้ Draw บน IRenderSystem) โดยไม่ขึ้นอยู่กับว่าผลลัพธ์จะสำเร็จได้อย่างไร

เช่นนำการใช้งานของฉันไปใช้ IMovementSystem:

public interface IGameMap
{
    string LeftOf(int[] currentPosition);
    string RightOf(int[] currentPosition);
    string UpOf(int[] currentPosition);
    string DownOf(int[] currentPosition);
}

public class MovementSystem : IMovementSystem
{
    private readonly IGameMap _gameMap;
    private int[] _previousPosition;
    private readonly int[] _currentPosition;
    public MovementSystem(IGameMap gameMap, int[] initialPosition)
    {
        _gameMap = gameMap;
        _currentPosition = initialPosition;
        _previousPosition = initialPosition;
    }

    public bool CanMoveLeft()
    {
        return _gameMap.LeftOf(_currentPosition) == "Unoccupied";
    }

    public void MoveLeft()
    {
        _previousPosition = _currentPosition;
        _currentPosition[0]--;
    }

    public bool CanMoveRight()
    {
        return _gameMap.RightOf(_currentPosition) == "Unoccupied";
    }

    public void MoveRight()
    {
        _previousPosition = _currentPosition;
        _currentPosition[0]++;
    }

    public bool CanMoveUp()
    {
        return _gameMap.UpOf(_currentPosition) == "Unoccupied";
    }

    public void MoveUp()
    {
        _previousPosition = _currentPosition;
        _currentPosition[1]--;
    }

    public bool CanMoveDown()
    {
        return _gameMap.DownOf(_currentPosition) == "Unoccupied";
    }

    public void MoveDown()
    {
        _previousPosition = _currentPosition;
        _currentPosition[1]++;
    }

    public bool Moved()
    {
        return _previousPosition == _currentPosition;
    }

    public int[] Position
    {
        get { return _currentPosition; }
    }
}

MovementSystemสามารถมีการพึ่งพาของตัวเองและPlayerจะไม่สนใจ โดยใช้อินเทอร์เฟซคุณสามารถสร้างเครื่องเกมสถานะ:

public class GameEngine
{
    private readonly List<IEntity> _entities;
    private List<IEntity> _renderQueue; 

    public GameEngine()
    {
        _entities = new List<IEntity>();
    }

    public void RegisterEntity(IEntity entity)
    {
        _entities.Add(entity);
    }

    public void Update()
    {
        _renderQueue = new List<IEntity>();
        foreach (var entity in _entities)
        {
            if(entity.Update())
                _renderQueue.Add(entity);
        }
        // Linq version for those interested
        //_renderQueue.AddRange(_entities.Where(e => e.Update()));
    }

    public void Render()
    {
        foreach (var entity in _renderQueue)
        {
            entity.Render();
        }
    }
}

และนั่นคือจุดเริ่มต้นของเกมที่น่ารัก (นั่นก็คือหน่วยทดสอบได้)

และด้วยการเพิ่มเติมบางอย่างและความแตกต่างบางอย่าง:

public interface IEntity
{
}

public interface IRenderableEntity : IEntity
{
    void Render();        
}

public interface IUpdateableEntity : IEntity
{
    void Update();
    bool Updated { get; }
}

public interface IRenderSystem
{
    void Draw(IRenderableEntity entity);
}

// new player class
public class Player : IRenderableEntity, IUpdateableEntity
{
    private readonly IInputSystem _inputSystem;
    private readonly IMovementSystem _movementSystem;
    private readonly IRenderSystem _renderSystem;
    private readonly int[] _size = new[] { 10, 10 };

    public Player(IRenderSystem renderSystem, IMovementSystem movementSystem, IInputSystem inputSystem)
    {
        _renderSystem = renderSystem;
        _movementSystem = movementSystem;
        _inputSystem = inputSystem;
    }

    public void Update()
    {
        if (_inputSystem.Direction == "Left" && _movementSystem.CanMoveLeft())
            _movementSystem.MoveLeft();
        if (_inputSystem.Direction == "Right" && _movementSystem.CanMoveRight())
            _movementSystem.MoveRight();
        if (_inputSystem.Direction == "Up" && _movementSystem.CanMoveUp())
            _movementSystem.MoveUp();
        if (_inputSystem.Direction == "Down" && _movementSystem.CanMoveDown())
            _movementSystem.MoveDown();
    }

    public bool Updated
    {
        get { return _movementSystem.Moved(); }
    }

    public void Render()
    {
        if (_movementSystem.Moved())
            _renderSystem.Draw(this);
    }

    public int[] Position
    {
        get { return _movementSystem.Position; }
    }

    public int[] Size
    {
        get { return _size; }
    }
}

public class GameEngine
{
    private readonly List<IEntity> _entities;
    private List<IRenderableEntity> _renderQueue; 

    public GameEngine()
    {
        _entities = new List<IEntity>();
    }

    public void RegisterEntity(IEntity entity)
    {
        _entities.Add(entity);
    }

    public void Update()
    {
        _renderQueue = new List<IRenderableEntity>();
        foreach (var entity in _entities)
        {
            if (entity is IUpdateableEntity)
            {
                var updateEntity = entity as IUpdateableEntity;
                updateEntity.Update();
            }

            if (entity is IRenderableEntity)
            {
                var renderEntity = entity as IRenderableEntity;
                _renderQueue.Add(renderEntity);
            }
        }
    }

    public void Render()
    {
        foreach (var entity in _renderQueue)
        {
            entity.Render();
        }
    }
}

ขณะนี้เรามีระบบเอนทิตี / ส่วนประกอบดั้งเดิมที่อิงกับอินเทอร์เฟซรวมและการสืบทอดแบบหลวม


1
นี่เป็นสิ่งที่ตรงกันข้ามกับการออกแบบส่วนประกอบ :) คุณจะทำอย่างไรถ้าคุณต้องการให้ผู้เล่นคนหนึ่งทำเสียงและอื่น ๆ ไม่ได้?
Kikaimaru

@Kikaimaru Pass ใน ISoundSystem ที่ไม่เล่นเสียง ie ไม่ทำอะไรเลย
ดัสติน Kingen

3
-1 ไม่ใช่เพราะมันเป็นรหัสที่ไม่ดี แต่เนื่องจากมันไม่เกี่ยวข้องกับสถาปัตยกรรมที่อิงองค์ประกอบทั้งหมด - อันที่จริงแล้วมันเป็นส่วนขยายของอินเทอร์เฟซเช่นนี้ที่ส่วนประกอบพยายามหลีกเลี่ยง
Kylotan

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