ฉันจะหลีกเลี่ยงการแต่งงานคับใน Unity ได้อย่างไร


24

เมื่อหลายปีก่อนฉันเริ่มทำงานกับ Unity และฉันยังคงต่อสู้กับปัญหาของสคริปต์คู่ที่แน่น ฉันจะจัดโครงสร้างรหัสของฉันเพื่อหลีกเลี่ยงปัญหานี้ได้อย่างไร

ตัวอย่างเช่น:

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

ฉันจะตามปกติสร้างการเชื่อมต่อเช่นIWalking, IHealthและIDeathเพื่อที่ฉันสามารถเปลี่ยนองค์ประกอบเหล่านี้ที่ราชประสงค์โดยไม่ทำลายส่วนที่เหลือของรหัสของฉัน PlayerScriptDependancyInjectorฉันจะมีพวกเขาตั้งขึ้นโดยสคริปต์แยกวัตถุเล่นพูด บางทีนั่นอาจจะมีสคริปต์สาธารณะIWalking, IHealthและIDeathคุณลักษณะเพื่อให้การอ้างอิงที่สามารถตั้งค่าโดยนักออกแบบระดับจากการตรวจสอบโดยการลากและวางสคริปต์ที่เหมาะสม

นั่นจะทำให้ฉันสามารถเพิ่มพฤติกรรมให้กับวัตถุเกมได้อย่างง่ายดายและไม่ต้องกังวลกับการพึ่งพารหัสฮาร์ดโค้ด

ปัญหาใน Unity

ปัญหาที่อยู่ในความสามัคคีฉันไม่สามารถสัมผัสอินเตอร์เฟซในการตรวจสอบและถ้าผมเขียนตรวจสอบของตัวเองอ้างอิงเคยได้รับต่อเนื่องและเป็นจำนวนมากของการทำงานที่ไม่จำเป็น นั่นเป็นเหตุผลที่ฉันทิ้งไว้กับการเขียนรหัสคู่แน่น Deathสคริปต์ของฉันแสดงการอ้างอิงถึงInertiveWalkingสคริปต์ แต่ถ้าฉันตัดสินใจว่าฉันต้องการให้ผู้เล่นควบคุมตัวละครให้แน่นฉันไม่สามารถลากและวางTightWalkingสคริปต์ได้ฉันต้องเปลี่ยนDeathสคริปต์ ที่ดูด ฉันสามารถจัดการกับมันได้ แต่วิญญาณของฉันร้องไห้ทุกครั้งที่ฉันทำอะไรแบบนี้

ทางเลือกที่ต้องการไปยังอินเตอร์เฟสใน Unity คืออะไร? ฉันจะแก้ไขปัญหานี้ได้อย่างไร ฉันพบสิ่งนี้แต่มันบอกสิ่งที่ฉันรู้แล้วและมันไม่ได้บอกวิธีทำใน Unity! สิ่งนี้จะกล่าวถึงสิ่งที่ควรทำไม่ใช่วิธีการที่มันไม่ได้แก้ไขปัญหาการมีเพศสัมพันธ์อย่างแน่นหนาระหว่างสคริปต์

โดยรวมแล้วฉันรู้สึกว่ามันถูกเขียนขึ้นสำหรับผู้ที่มาที่ Unity ด้วยพื้นหลังการออกแบบเกมที่เพิ่งเรียนรู้วิธีเขียนโค้ดและมีทรัพยากรน้อยมากใน Unity สำหรับนักพัฒนาทั่วไป มีวิธีมาตรฐานในการจัดโครงสร้างโค้ดของคุณใน Unity หรือฉันต้องหาวิธีการของตัวเอง?


1
The problem is that in Unity I can't expose interfaces in the inspectorฉันไม่คิดว่าฉันเข้าใจสิ่งที่คุณหมายถึงโดย "เปิดเผยส่วนต่อประสานงานในผู้ตรวจสอบ" เพราะความคิดแรกของฉันคือ "ทำไมไม่"
jhocking

@ jocking ถาม devs สามัคคี คุณไม่เห็นมันในสารวัตร มันจะไม่ปรากฏขึ้นหากคุณตั้งค่าเป็นคุณลักษณะสาธารณะและคุณลักษณะอื่น ๆ ทั้งหมดจะทำ
KL

1
โอ้คุณหมายถึงการใช้อินเทอร์เฟซเป็นชนิดของตัวแปรไม่ใช่แค่อ้างอิงวัตถุด้วยอินเตอร์เฟสนั้น
jhocking

1
คุณอ่านบทความ ECS ดั้งเดิมแล้วหรือยัง? cowboyprogramming.com/2007/01/05/evolve-your-heirachyแล้วการออกแบบ SOLID ล่ะ? en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
jzx

1
@jzx ฉันรู้และใช้การออกแบบ SOLID และฉันเป็นแฟนตัวยงของหนังสือ Robert C. Martins แต่คำถามนี้เกี่ยวกับการทำเช่นนั้นใน Unity และไม่ฉันไม่ได้อ่านบทความที่อ่านมันตอนนี้ขอบคุณ :)
KL

คำตอบ:


14

มีสองสามวิธีที่คุณสามารถทำงานเพื่อหลีกเลี่ยงการเชื่อมต่อสคริปต์อย่างแน่นหนา Internal to Unity เป็นฟังก์ชั่น SendMessage ที่เมื่อกำหนดเป้าหมายที่ GameObject ของ Monobehaviour ส่งข้อความนั้นไปยังทุกสิ่งบนวัตถุของเกม ดังนั้นคุณอาจมีสิ่งนี้ในวัตถุสุขภาพของคุณ:

[SerializeField]
private int _currentHealth;
public int currentHealth
{
    get { return _currentHealth;}
    protected set
    {
        _currentHealth = value;
        gameObject.SendMessage("HealthChanged", value, SendMessageOptions.DontRequireReceiver);
    }
}

[SerializeField]
private int _maxHealth;
public int maxHealth
{
    get { return _maxHealth;}
    protected set
    {
        _maxHealth = value;
        if (currentHealth > _maxHealth)
        {
            currentHealth = _maxHealth;
        }
    }
}

public void ChangeHealth(int deltaChange)
{
    currentHealth += deltaChange;
}

มันง่ายมาก แต่ควรแสดงให้เห็นอย่างชัดเจนว่าฉันกำลังทำอะไรอยู่ สถานที่ให้บริการโยนข้อความเช่นเดียวกับที่คุณทำเหตุการณ์ สคริปต์มรณะของคุณจะสกัดกั้นข้อความนี้ (และหากไม่มีสิ่งใดขวางไว้ SendMessageOptions.DontRequireReceiver จะรับประกันว่าคุณจะไม่ได้รับข้อยกเว้น) และดำเนินการตามค่าสุขภาพใหม่หลังจากที่ตั้งค่าไว้แล้ว ชอบมาก

void HealthChanged(int newHealth)
{
    if (newHealth <= 0)
    {
        Die();
    }
}

void Die ()
{
    Debug.Log ("I am undone!");
    DestroyObject (gameObject);
}

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

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

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


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

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

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

7

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

http://forum.unity3d.com/threads/free-vfw-full-set-of-drawers-savesystem-serialize-interfaces-generics-auto-props-delegates.266165/

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

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


2
โดยส่วนตัวแล้วฉันมักจะพึ่งพาปลั๊กอินและไลบรารีมากเกินไปสำหรับสิ่งระดับต่ำเช่นนี้ ฉันอาจจะหวาดระแวงเล็กน้อย แต่ก็รู้สึกว่ามีความเสี่ยงมากเกินไปที่จะทำให้โปรเจ็กต์ของฉันพังเพราะโค้ดของพวกเขาแตกหลังจากอัปเดต Unity
jhocking

ฉันใช้เฟรมเวิร์กนั้นและในที่สุดก็หยุดเพราะฉันมีเหตุผลที่ @hocking กล่าวถึงที่อยู่ด้านหลังของใจ (และมันก็ไม่ได้ช่วยอะไรตั้งแต่การอัปเดตครั้งหนึ่งไปจนถึงอีกอัปเดต แต่อ่างครัวในขณะที่ทำลายความเข้ากันได้แบบย้อนกลับ [และลบคุณลักษณะที่มีประโยชน์หรือสอง])
Selali Adobor

6

วิธีการเฉพาะ Unity ที่เกิดขึ้นกับฉันคือการเริ่มต้นGetComponents<MonoBehaviour>()เพื่อรับรายการสคริปต์จากนั้นแปลงสคริปต์เหล่านั้นให้เป็นตัวแปรส่วนตัวสำหรับส่วนต่อประสานที่เฉพาะเจาะจง คุณต้องการทำเช่นนี้ใน Start () กับสคริปต์หัวฉีดพึ่งพา สิ่งที่ต้องการ:

MonoBehaviour[] list = GetComponents<MonoBehaviour>();
for (int i = 0; i < list.Length; i++) {
    if (list[i] is IHealth) {
        Debug.Log("Found it!");
    }
}

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


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


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

สำหรับการแก้ไข - เมื่อฉันมีรายการสคริปต์รหัสของฉันจะรู้ได้อย่างไรว่าสคริปต์ตัวไหนที่ไม่สามารถอินเทอร์เฟซใด
KL

1
แก้ไขเพื่ออธิบายเช่นกัน
jhocking

1
ใน Unity 5 คุณสามารถใช้GetComponent<IMyInterface>()และGetComponents<IMyInterface>()โดยตรงซึ่งส่งคืนส่วนประกอบที่ใช้งาน
S. TarıkÇetin

5

จากประสบการณ์ของฉันเองเกม dev แบบดั้งเดิมเกี่ยวข้องกับวิธีปฏิบัติมากกว่า dev อุตสาหกรรม (มีเลเยอร์ abstraction น้อยกว่า) ส่วนหนึ่งเป็นเพราะคุณต้องการสนับสนุนประสิทธิภาพมากกว่าการบำรุงรักษาและเพราะคุณมีโอกาสน้อยที่จะนำรหัสของคุณกลับมาใช้ในบริบทอื่น ๆ (โดยปกติแล้วจะมีการเชื่อมต่อที่แข็งแกร่งระหว่างกราฟิกและตรรกะของเกม)

ฉันเข้าใจถึงความกังวลของคุณโดยเฉพาะอย่างยิ่งเพราะความกังวลเรื่องประสิทธิภาพไม่สำคัญกับอุปกรณ์ล่าสุด แต่ความรู้สึกของฉันคือคุณจะต้องพัฒนาโซลูชันของคุณเองถ้าคุณต้องการใช้แนวปฏิบัติที่ดีที่สุดของ OOP ในอุตสาหกรรมเพื่อพัฒนาเกม Unity ฯลฯ ... )


2

ดูที่ IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ) ซึ่งให้คุณเปิดเผยส่วนต่อประสานในโปรแกรมแก้ไขเช่นเดียวกับที่คุณพูดถึง มีการเดินสายเพิ่มขึ้นอีกเล็กน้อยเมื่อเทียบกับการประกาศตัวแปรปกตินั่นคือทั้งหมด

public interface IPinballValue{
   void Add(int value);
}

[System.Serializable]
public class IPinballValueContainer : IUnifiedContainer<IPinballValue> {}

public class MyClassThatExposesAnInterfaceProperty : MonoBehaviour {
   [SerializeField]
   IPinballValueContainer value;
}

นอกจากนี้ยังมีคำตอบอื่นที่กล่าวถึงการใช้ SendMessage ไม่ได้ลบการเชื่อมต่อ แต่มันทำให้มันไม่ผิดพลาดในเวลารวบรวม แย่มาก - เมื่อคุณขยายโครงการของคุณมันอาจกลายเป็นฝันร้ายในการรักษา

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


1

ที่มา: http://www.udellgames.com/posts/ultra-useful-unity-snippet-developers-use-interfaces/

[SerializeField]
private GameObject privateGameObjectName;

public GameObject PublicGameObjectName
{
    get { return privateGameObjectName; }
    set
    {
        //Make sure changes to privateGameObjectName ripple to privateInterfaceName too
        if (privateGameObjectName != value)
        {
            privateInterfaceName = null;
            privateGameObjectName = value;
        }
    }
}

private InterfaceType privateInterfaceName;
public InterfaceType PublicInterfaceName
{
    get
    {
        if (privateInterfaceName == null)
        {
            privateInterfaceName = PublicGameObjectName.GetComponent(typeof(InterfaceType)) as InterfaceType;
        }
        return privateInterfaceName;
    }
    set
    {
        //Make sure changes to PublicInterfaceName ripple to PublicGameObjectName too
        if (privateInterfaceName != value)
        {
            privateGameObjectName = (privateInterfaceName as Component).gameObject;
            privateInterfaceName = value;
        }

    }
}

0

ฉันใช้กลยุทธ์ที่แตกต่างกันสองสามข้อเพื่อหลีกเลี่ยงข้อ จำกัด ที่คุณพูดถึง

หนึ่งในผู้ที่ฉันไม่เห็นกล่าวถึงคือการใช้MonoBehaviorที่เปิดเผยการเข้าถึงการใช้งานที่แตกต่างกันของอินเตอร์เฟซที่กำหนด ตัวอย่างเช่นฉันมีอินเทอร์เฟซที่กำหนดวิธีการใช้ชื่อ RaycastsIRaycastStrategy

public interface IRaycastStrategy 
{
    bool Cast(Ray ray, out RaycastHit raycastHit, float distance, LayerMask layerMask);
}

จากนั้นผมก็ใช้มันในการเรียนที่แตกต่างกันกับการเรียนเช่นLinecastRaycastStrategyและSphereRaycastStrategyและใช้ MonoBehavior RaycastStrategySelectorที่ exposes เช่นเดียวของแต่ละประเภท บางสิ่งที่คล้ายRaycastStrategySelectionBehaviorกันซึ่งถูกนำไปใช้งานเช่น:

public class RaycastStrategySelectionBehavior : MonoBehavior
{

    private readonly Dictionary<RaycastStrategyType, IRaycastStrategy> _raycastStrategies
        = new Dictionary<RaycastStrategyType, IRaycastStrategy>
        {
            { RaycastStrategyType.Line, new LineRaycastStrategy() },
            { RaycastStrategyType.Sphere, new SphereRaycastStrategy() }
        };

    public RaycastStrategyType StrategyType;


    public IRaycastStrategy RaycastStrategy
    {
        get
        {
            IRaycastStrategy raycastStrategy;
            if (!_raycastStrategies.TryGetValue(StrategyType, out raycastStrategy))
            {
                throw new InvalidOperationException("There is no registered implementation for the selected strategy");
            }
            return raycastStrategy;
        }
    }
}

หากการใช้งานมีแนวโน้มที่จะอ้างอิงฟังก์ชั่น Unity ในการสร้างของพวกเขา (หรือเพียงเพื่อความปลอดภัยฉันเพียงแค่ลืมสิ่งนี้เมื่อพิมพ์ตัวอย่างนี้) ใช้Awakeเพื่อหลีกเลี่ยงปัญหาเกี่ยวกับการเรียก constructors หรือการอ้างอิง Unity ด้าย

กลยุทธ์นี้มีข้อบกพร่องบางอย่างโดยเฉพาะเมื่อคุณต้องจัดการการใช้งานที่แตกต่างกันหลายอย่างที่เปลี่ยนแปลงอย่างรวดเร็ว ปัญหาที่เกิดจากการขาดการตรวจสอบเวลาคอมไพล์สามารถช่วยได้โดยใช้โปรแกรมแก้ไขที่กำหนดเองเนื่องจากค่า enum สามารถต่อเนื่องกันได้โดยไม่ต้องทำงานพิเศษใด ๆ (แม้ว่าฉันมักจะลืมเรื่องนี้ไปแล้ว

ฉันใช้กลยุทธ์นี้เนื่องจากความยืดหยุ่นที่ช่วยให้คุณอยู่บนพื้นฐานของ MonoBeh พฤติกรรมโดยไม่บังคับให้คุณใช้อินเทอร์เฟซโดยใช้ MonoBehaviors สิ่งนี้จะช่วยให้คุณ "ดีที่สุดของทั้งสองโลก" เนื่องจากตัวเลือกสามารถเข้าถึงได้โดยใช้GetComponentการจัดลำดับทั้งหมดจะถูกจัดการโดย Unity และตัวเลือกสามารถใช้ตรรกะในเวลาทำงานเพื่อสลับการใช้งานโดยอัตโนมัติตามปัจจัยบางอย่าง

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