Unity에서 긴밀한 스크립트 연결을 피하려면 어떻게해야합니까?


24

얼마 전에 나는 Unity로 작업하기 시작했지만 여전히 밀접하게 연결된 스크립트 문제로 어려움을 겪고 있습니다. 이 문제를 피하기 위해 코드를 어떻게 구성 할 수 있습니까?

예를 들면 다음과 같습니다.

건강과 사망 시스템을 별도의 스크립트로 만들고 싶습니다. 또한 플레이어 캐릭터가 움직이는 방식 (마리오에서와 같은 물리 기반의 관성 컨트롤과 수퍼 미트 보이 (Super Meat Boy)에서와 같이 빡빡하고 경악 한 컨트롤)을 변경할 수있는 서로 다른 교환식 보행 스크립트를 갖고 싶습니다. Health 스크립트는 플레이어의 체력이 0에 도달 할 때 Die () 메서드를 트리거 할 수 있도록 Death 스크립트에 대한 참조를 보유해야합니다. Death 스크립트는 사용 된 워킹 스크립트에 대한 참조를 보유해야합니다. 좀비에 질려 요).

나는 것이 일반적으로 같은 인터페이스를 만들고 IWalking, IHealth그리고 IDeath나는 내 코드의 나머지 부분을 파괴하지 않고 충동에서 이러한 요소를 변경할 수 있습니다 그래서. 플레이어 객체에 별도의 스크립트로 설정하도록하십시오 PlayerScriptDependancyInjector. 아마 스크립트를 공개해야한다고 IWalking, IHealth그리고 IDeath의존성을 끌어 적절한 스크립트를 놓아 관리자에서 레벨 디자이너에 의해 설정 될 수 그래서, 속성을.

이를 통해 게임 오브젝트에 동작을 쉽게 추가하고 하드 코딩 된 종속성에 대해 걱정하지 않아도됩니다.

유니티의 문제

문제는 유니티에서 나는 관리자의 인터페이스를 노출 할 수 없다는 것입니다, 나는 내 자신의 사찰을 작성하는 경우, 참조 직렬화 얻을 실 거예요, 그것은 불필요한 작업을 많이합니다. 그렇기 때문에 단단히 결합 된 코드를 작성해야합니다. 내 Death스크립트는 스크립트에 대한 참조를 노출합니다 InertiveWalking. 그러나 플레이어 캐릭터가 엄격하게 제어하기를 원한다면 TightWalking스크립트를 끌어서 놓을 수 없으므로 Death스크립트 를 변경해야합니다 . 짜증나 나는 그것을 다룰 수 있지만, 나는 이런 일을 할 때마다 내 영혼이 울다.

Unity 인터페이스보다 선호되는 대안은 무엇입니까? 이 문제를 어떻게 해결합니까? 내가 발견 있지만, 이미 알고있는 것을 알려줍니다, 그리고 그것을 어떻게 유니티에서 그렇게 말해하지 않습니다! 이것 역시 어떻게해야하는지, 어떻게해야하는지에 대해 논의합니다.

대체로 코딩 방법을 배우는 게임 디자인 배경을 가지고 Unity에 온 사람들을 위해 작성된 것으로 생각하며 일반 개발자를 위해 Unity에 대한 리소스는 거의 없습니다. Unity에서 코드를 구성하는 표준 방법이 있습니까, 아니면 내 방법을 찾아야합니까?


1
The problem is that in Unity I can't expose interfaces in the inspector내 생각이 "왜 안돼"였기 때문에 "검사관의 인터페이스 노출"이 무슨 뜻인지 이해하지 못하는 것 같습니다.
jhocking 님이

@jhocking은 화합 개발자에게 물어보십시오. 당신은 단지 관리자에서 그것을 볼 수 없습니다. 공용 속성으로 설정하면 다른 속성 만 표시됩니다.
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

엄격한 스크립트 커플 링을 피하기 위해 작업 할 수있는 몇 가지 방법이 있습니다. Unity의 내부는 MonoBehaviour의 GameObject를 대상으로 할 때 해당 메시지를 게임 오브젝트의 모든 것에 보내는 SendMessage 함수입니다. 따라서 건강 개체에 다음과 같은 것이있을 수 있습니다.

[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의 최상위 스크립트에서 대부분의 스크립트로 이동하지만 자신이 관찰 할 수없는 것은 없습니다.

컴포넌트를 가져오고 Type 대신 특정 인터페이스가있는 컴포넌트 만 반환하는 커스텀 GameObject 함수를 빌드하는 다른 솔루션이 있습니다. 시간이 좀 더 걸리지 만 목적에 잘 부합 할 수 있습니다.

또한 실제로 할당을 커밋하기 전에 해당 인터페이스의 편집기 위치에 할당 된 개체를 확인하는 사용자 지정 편집기를 작성할 수 있습니다 (이 경우 스크립트를 직렬화 할 수 있도록 스크립트를 정상적으로 할당하지만 오류가 발생 함) 올바른 인터페이스를 사용하지 않고 할당을 거부하는 경우). 나는이 두 가지를 모두 수행했으며 해당 코드를 생성 할 수 있지만 먼저 코드를 찾는 데 시간이 필요합니다.


5
SendMessage ()는이 상황을 해결하고 많은 상황에서 해당 명령을 사용하지만 이와 같은 대상이 지정되지 않은 메시지 시스템은 수신자에 대한 참조를 직접 갖는 것보다 덜 효율적입니다. 객체 간의 모든 일상적인 통신에이 명령을 사용해야합니다.
jhocking 2019

2
저는 Component 객체가 더 많은 내부 코어 시스템에 대해 알고 있지만 핵심 시스템은 구성 요소에 대해 아무것도 모르는 이벤트 및 델리게이트 사용에 대한 개인적인 팬입니다. 그러나 그들이 대답을 원하는 방식으로 100 % 확신하지 못했습니다. 그래서 스크립트를 완전히 분리하는 방법에 대한 답변을 얻었습니다.
Brett Satoru

1
또한 Unity Inspector에서 지원하지 않는 인터페이스 및 기타 프로그래밍 구성을 노출 할 수있는 Unity 애셋 스토어에 사용 가능한 플러그인이 있다고 생각합니다. 빠른 검색이 필요합니다.
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의 스크립트 지원이 부족하여 디자인을 손상시키지 마십시오. 기본적으로 Unity에 있어야합니다.

이것이 옵션이 아닌 경우, 단일 동작 클래스를 끌어서 놓아 필요한 인터페이스로 동적으로 캐스트하십시오. 작업이 많고 우아하지는 않지만 작업이 완료됩니다.


2
개인적으로 나는 이와 같은 저수준의 것들을 위해 플러그인과 라이브러리에 너무 의존하는 경향이 있습니다. 아마도 약간의 편집증 일지 모르지만 Unity 업데이트 후 코드가 깨지기 때문에 프로젝트가 중단 될 위험이 너무 큽니다.
jhocking

나는 그 프레임 워크를 사용하고 있었고 @jhocking이 내 마음 뒤에서 아 먹는 것을 언급 한 이유가 있었기 때문에 결국 멈추었다. (그리고 그것은 한 업데이트에서 다른 업데이트로 도움이되지 않았다. 하지만 부엌 싱크대 동안 이전 버전과의 호환성을 깨는 [하고 오히려 유용한 기능 또는 두 개의 제거])
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 () 명령과 'Type'type을 사용하여 개별 구성 요소가 종속성 주입 스크립트에 종속성이 무엇인지 알 수 있습니다. 즉, 구성 요소는 종속성 인젝터에 유형 목록을 제공 한 다음 종속성 인젝터가 해당 유형의 객체 목록을 반환합니다.


또는 인터페이스 대신 추상 클래스를 사용할 수도 있습니다. 추상 클래스는 인터페이스와 거의 같은 목적을 달성 할 수 있으며 인스펙터에서이를 참조 할 수 있습니다.


여러 인터페이스를 구현할 수있는 동안 여러 클래스에서 상속 할 수 없습니다. 이것은 문제가 될 수 있지만 아무것도 아닌 것보다 훨씬 낫습니다 :) 답변 주셔서 감사합니다!
KL

편집에 관해서는-스크립트 목록이 있으면 코드에서 어떤 인터페이스에 어떤 스크립트를 사용할 수 없는지 어떻게 알 수 있습니까?
KL

1
그것을 설명하기 위해 편집 됨
jhocking

1
유니티 5에서는 사용할 수 GetComponent<IMyInterface>()GetComponents<IMyInterface>()직접 whch 반환 구성 요소 (들) 그 구현을.
S. Tarık Çetin

5

내 경험에 비추어 볼 때, 게임 개발은 전통적으로 산업 개발 (보다 추상화 계층이 적은)보다 더 실용적인 접근 방식을 사용합니다. 부분적으로 유지 관리 성보다 성능을 선호하고 다른 컨텍스트에서 코드를 재사용 할 가능성이 적기 때문에 (일반적으로 그래픽과 게임 로직 사이에 강한 본질적인 결합이 있습니다)

특히 최근 장치에서는 성능 문제가 덜 중요하기 때문에 우려 사항을 완전히 이해하고 있지만 산업용 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주어진 인터페이스의 다른 구현에 대한 액세스를 제공하는를 사용하는 것 입니다. 예를 들어, Raycast가 어떻게 구현되는지 정의하는 인터페이스가 있습니다.IRaycastStrategy

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

나는 그 클래스가 좋아 서로 다른 클래스에 구현 LinecastRaycastStrategy하고 SphereRaycastStrategy하고 MonoBehavior 구현 RaycastStrategySelector각 유형의 단일 인스턴스를 노출합니다. 뭐 그런 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편집기에서 또는 주 외부에서 생성자를 호출하거나 Unity 함수를 참조하는 문제를 피하기 위해 사용 하십시오 실

이 전략은 특히 빠르게 변화하는 많은 다른 구현을 관리해야 할 때 몇 가지 단점이 있습니다. 컴파일 시간 검사가 부족한 문제는 사용자 정의 편집기를 사용하여 도움이 될 수 있습니다. 열거 형 값은 특별한 작업없이 직렬화 될 수 있기 때문에 (일반적으로 구현을 그렇게 많이하지 않기 때문에 이것을 잊어 버릴지라도).

실제로 MonoBehaviors를 사용하여 인터페이스를 구현하지 않고도 MonoBehaviors를 기반으로하는 유연성으로 인해이 전략을 사용합니다. Selector는를 사용하여 액세스 할 수 있고 GetComponentUnity는 직렬화를 모두 처리하며 Selector는 런타임에 로직을 적용하여 특정 요소를 기반으로 구현을 자동으로 전환 할 수 있기 때문에 "두 세계의 최고"를 제공합니다 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.