캐릭터의 기술과 능력을 명령으로 사용하는 것이 좋습니까?


11

나는 독특한 공격 기술과 건물, 수리 등의 다른 능력을 가진 캐릭터로 구성된 게임을 설계하고 있습니다. 플레이어는 여러 캐릭터를 제어 할 수 있습니다.

나는 그러한 기술과 능력을 모두 개별 명령에 넣을 생각입니다. 정적 컨트롤러는 이러한 모든 명령을 정적 명령 목록에 등록합니다. 정적 목록은 게임 내 모든 캐릭터의 모든 기술과 능력으로 구성됩니다. 따라서 플레이어가 캐릭터 중 하나를 선택하고 UI에서 버튼을 클릭하여 주문을 시전하거나 능력을 수행하면 View는 정적 컨트롤러를 호출하여 목록에서 원하는 명령을 가져 와서 실행합니다.

그러나 이것이 Unity에서 게임을 빌드하고 있다고 생각할 때 이것이 좋은 디자인인지 확실하지 않습니다. 나는 모든 기술과 능력을 개별 구성 요소로 만들 수 있었고 게임의 캐릭터를 나타내는 GameObjects에 첨부 할 수 있다고 생각합니다. 그런 다음 UI는 캐릭터의 GameObject 를 잡고 명령을 실행해야합니다.

내가 디자인하고있는 게임에 대해 더 나은 디자인과 연습은 무엇입니까?


좋아 보여! 이 관련 사실을 그냥 내버려 두십시오. 일부 언어에서는 각 명령을 자체 기능으로 만드는 것까지 가능합니다. 입력을 쉽게 자동화 할 수 있기 때문에 테스트에는 몇 가지 놀라운 이점이 있습니다. 또한 콜백 함수 변수를 다른 명령 함수에 다시 할당하여 제어 리 바인딩을 쉽게 수행 할 수 있습니다.
Anko

@Anko, 모든 명령을 정적 목록에 넣은 부분은 어떻습니까? 나는 목록이 커질지도 모른다는 걱정이 들며 명령이 필요할 때마다 엄청난 명령 목록을 쿼리해야합니다.
크세논

1
@xenon이 코드 부분에서 성능 문제가 발생하지 않을 가능성이 큽니다. 사용자 상호 작용 당 한 번만 발생할 수있는 한, 성능에 눈에 띄게 흠집을 내려면 계산 집약적이어야합니다.
aaaaaaaaaaaa

답변:


17

TL; DR

이 답변은 약간 미쳐갑니다. 그러나 C ++ / Java / .NET 디자인 패턴을 의미하는 코드로 접근하는 방법을 의미하는 "명령"으로 기능을 구현하는 것에 대해 이야기하고 있기 때문입니다. 그 접근법은 유효하지만 더 좋은 방법이 있습니다. 어쩌면 당신은 이미 다른 방법을하고있을 것입니다. 그렇다면, 오 잘. 희망이 있다면 다른 사람들이 유용하다고 생각합니다.

추격을 줄이기 위해 아래의 데이터 중심 접근 방식을보십시오. Jacob Pennock의 CustomAssetUility를 여기에서 받아 보시고 그의 게시물을 읽으 십시오 .

Unity 작업

다른 사람들이 언급했듯이 100-300 개의 항목을 순회하는 것은 생각만큼 큰 것이 아닙니다. 이것이 직관적 인 접근 방법이라면 그렇게하십시오. 두뇌 효율성을 최적화하십시오. 그러나 @Norguard가 그의 답변 에서 입증 한 것처럼 Dictionary 는 일정한 시간 동안 삽입하고 검색하기 때문에 문제를 제거하기위한 쉬운 방법입니다. 아마 그것을 사용해야합니다.

Unity 내 에서이 작업을 잘 수행한다는 측면에서 내 직감은 능력 당 하나의 MonoBehaviour가 내려가는 위험한 길이라고 말합니다. 어떤 능력이 시간이 지남에 따라 상태를 유지하는 경우 해당 상태를 재설정 할 수있는 방법을 제공해야합니다. 코 루틴은이 문제를 완화하지만, 여전히 해당 스크립트의 모든 업데이트 프레임에서 IEnumerator 참조를 관리하고 있으며, 불완전하고 상태 루프에 빠지지 않도록 기능을 재설정 할 수있는 확실한 방법이 있는지 확인해야합니다. 게임이 눈에 띄지 않을 때 조용히 게임의 안정성을 망치기 시작합니다. "물론 내가 할게!" "나는 '좋은 프로그래머'입니다!"라고 말합니다. 그러나 실제로, 우리는 모두 객관적으로 끔찍한 프로그래머이며 위대한 AI 연구원과 컴파일러 작성자조차도 항상 문제를 일으 킵니다.

Unity에서 명령 인스턴스화 및 검색을 구현할 수있는 모든 방법 중 하나는 괜찮고 동맥류를주지 않으며 다른 하나는 무제한 마법 창조를 허용합니다 . 일종의.

코드 중심 접근법

첫 번째는 대부분 코드 내 방식입니다. 내가 추천하는 것은 각 명령을 BaseCommand abtract 클래스에서 상속하거나 ICommand 인터페이스를 구현하는 간단한 클래스로 만드는 것입니다. 다른 용도). 이 시스템은 각 명령이 ICommand라고 가정하고 매개 변수를 사용하지 않는 공용 생성자를 가지며 각 프레임이 활성화 된 동안 업데이트해야합니다.

추상 기본 클래스를 사용하면 상황이 더 간단하지만 내 버전은 인터페이스를 사용합니다.

MonoBehaviours는 하나의 특정 행동 또는 밀접한 관련 행동 시스템을 캡슐화하는 것이 중요합니다. 평범한 C # 클래스로 효과적으로 프록시하는 많은 MonoBehaviours를 갖는 것은 괜찮지 만, 너무 자신을 발견하면 XNA 게임처럼 보이기 시작하는 시점까지 모든 종류의 다른 객체에 대한 호출을 업데이트 할 수 있습니다. 심각한 문제가 발생하여 아키텍처를 변경해야합니다.

// ICommand.cs
public interface ICommand
{
    public void Execute(AbilityActivator originator, TargetingInfo targets);
    public void Update();
    public bool IsActive { get; }
}


// CommandList.cs
// Attach this to a game object in your loading screen
public static class CommandList
{
    public static ICommand GetInstance(string key)
    {
        return commandDict[key].GetRef();
    }


    static CommandListInitializerScript()
    {
        commandDict = new Dictionary<string, ICommand>() {

            { "SwordSpin", new CommandRef<SwordSpin>() },

            { "BellyRub", new CommandRef<BellyRub>() },

            { "StickyShield", new CommandRef<StickyShield>() },

            // Add more commands here
        };
    }


    private class CommandRef<T> where T : ICommand, new()
    {
        public ICommand GetNew()
        {
            return new T();
        }
    }

    private static Dictionary<string, ICommand> commandDict;
}


// AbilityActivator.cs
// Attach this to your character objects
public class AbilityActivator : MonoBehaviour
{
    List<ICommand> activeAbilities = new List<ICommand>();

    void Update()
    {
        string activatedAbility = GetActivatedAbilityThisFrame();
        if (!string.IsNullOrEmpty(acitvatedAbility))
            ICommand command = CommandList.Get(activatedAbility).GetRef();
            command.Execute(this, this.GetTargets());
            activeAbilities.Add(command);
        }

        foreach (var ability in activeAbilities) {
            ability.Update();
        }

        activeAbilities.RemoveAll(a => !a.IsActive);
    }
}

이것은 완전히 잘 작동하지만 더 잘 할 수 있습니다 (또한 List<T>시간 능력을 저장하기위한 최적의 데이터 구조가 아니므로 a LinkedList<T>또는 a를 원할 수도 있습니다 SortedDictionary<float, T>).

데이터 중심 접근

능력의 영향을 매개 변수화 할 수있는 논리적 행동으로 줄일 수 있습니다. 이것이 Unity가 실제로 구축 한 것입니다. 프로그래머는 자신이나 디자이너가 편집기에서 다양한 효과를 낼 수있는 시스템을 설계합니다. 이를 통해 코드의 "리깅"을 크게 단순화하고 기능 실행에만 집중할 수 있습니다. 기본 클래스 나 인터페이스 및 제네릭을 여기 저글링 할 필요가 없습니다. 그것은 모두 순전히 데이터 중심이 될 것입니다 (이는 명령 인스턴스 초기화를 단순화합니다).

가장 먼저 필요한 것은 능력을 설명 할 수있는 ScriptableObject입니다. ScriptableObjects는 훌륭합니다. Unity의 인스펙터에서 공개 필드를 설정할 수 있다는 점에서 MonoBehaviours와 같이 작동하도록 설계되었으며 이러한 변경 사항은 디스크에 직렬화됩니다. 그러나 어떤 오브젝트에도 부착되지 않으며 장면에서 게임 오브젝트에 부착하거나 인스턴스화 할 필요가 없습니다. 그것들은 Unity의 모든 데이터 버킷입니다. 표시된 기본 유형, 열거 형 및 단순 클래스 (상속 없음)를 직렬화 할 수 있습니다 [Serializable]. Structs는 Unity에서 직렬화 할 수 없으며 직렬화를 사용하면 검사기에서 객체 필드를 편집 할 수 있으므로 기억하십시오.

많은 것을 시도하는 ScriptableObject가 있습니다. 이것을 직렬화 된 클래스와 ScriptableObjects로 나눌 수는 있지만 수행하는 방법에 대한 아이디어를 제공해야합니다. 일반적으로 이것은 C #과 같은 멋진 현대 객체 지향 언어에서 추악하게 보입니다. 모든 열거 형에서 C89 똥과 같은 느낌이 들기 때문에 실제로 강력한 힘은 지원하기 위해 새로운 코드를 작성하지 않고도 모든 종류의 다른 능력을 만들 수 있다는 것입니다 그들. 그리고 첫 번째 형식이 필요한 작업을 수행하지 않으면 필요할 때까지 계속 추가하십시오. 필드 이름을 변경하지 않는 한 모든 이전의 직렬화 된 자산 파일은 계속 작동합니다.

// CommandAbilityDescription.cs
public class CommandAbilityDecription : ScriptableObject
{

    // Identification and information
    public string displayName; // Name used for display purposes for the GUI
    // We don't need an identifier field, because this will actually be stored
    // as a file on disk and thus implicitly have its own identifier string.

    // Description of damage to targets

    // I put this enum inside the class for answer readability, but it really belongs outside, inside a namespace rather than nested inside a class
    public enum DamageType
    {
        None,
        SingleTarget,
        SingleTargetOverTime,
        Area,
        AreaOverTime,
    }

    public DamageType damageType;
    public float damage; // Can represent either insta-hit damage, or damage rate over time (depend)
    public float duration; // Used for over-time type damages, or as a delay for insta-hit damage

    // Visual FX
    public enum EffectPlacement
    {
        CenteredOnTargets,
        CenteredOnFirstTarget,
        CenteredOnCharacter,
    }

    [Serializable]
    public class AbilityVisualEffect
    {
        public EffectPlacement placement;
        public VisualEffectBehavior visualEffect;
    }

    public AbilityVisualEffect[] visualEffects;
}

// VisualEffectBehavior.cs
public abtract class VisualEffectBehavior : MonoBehaviour
{
    // When an artist makes a visual effect, they generally make a GameObject Prefab.
    // You can extend this base class to support different kinds of visual effects
    // such as particle systems, post-processing screen effects, etc.
    public virtual void PlayEffect(); 
}

Damage 섹션을 Serializable 클래스로 추가로 추상화하여 피해를 입히거나 치유하는 능력을 정의하고 한 가지 능력으로 여러 가지 피해 유형을 가질 수 있습니다. 스크립트 가능한 여러 개체를 사용하고 디스크에서 서로 다른 복잡한 손상 구성 파일을 참조하지 않는 한 유일한 규칙은 상속이 아닙니다.

여전히 AbilityActivator MonoBehaviour가 필요하지만 이제는 더 많은 작업을 수행합니다.

// AbilityActivator.cs
public class AbilityActivator : MonoBehaviour
{
    public void ActivateAbility(string abilityName)
    {
        var command = (CommandAbilityDescription) Resources.Load(string.Format("Abilities/{0}", abilityName));
        ProcessCommand(command);
    }

    private void ProcessCommand(CommandAbilityDescription command)
    {

        foreach (var fx in command.visualEffects) {
            fx.PlayEffect();
        }

        switch(command.damageType) {
            // yatta yatta yatta
        }

        // and so forth, whatever your needs require

        // You could even make a copy of the CommandAbilityDescription
        var myCopy = Object.Instantiate(command);

        // So you can keep track of state changes (ie: damage duration)
    }
}

가장 멋진 부분

따라서 첫 번째 접근 방식의 인터페이스와 일반적인 속임수가 잘 작동합니다. 그러나 Unity를 최대한 활용하기 위해 ScriptableObjects를 사용하면 원하는 위치로 이동할 수 있습니다. Unity는 프로그래머에게 일관되고 논리적 인 환경을 제공한다는 점에서 훌륭하지만 GameMaker, UDK 등에서 얻은 디자이너와 아티스트를위한 모든 데이터 입력 기능도 갖추고 있습니다. 알.

지난 달, 우리 아티스트는 여러 종류의 원점 미사일에 대한 동작을 정의하는 파워 업 ScriptableObject 유형을 가져 와서 AnimationCurve와 결합하여 미사일을지면을 맴돌 게하여이 미친 새로운 회전 하키 퍽을 만들었습니다. 죽음의 무기.

여전히 돌아가서이 동작에 대한 특정 지원을 추가하여 효율적으로 실행되도록해야합니다. 그러나 우리는이 일반적인 데이터 기술 인터페이스를 만들었 기 때문에 그는이 아이디어를 허공에서 끌어내어 프로그래머가 자신이 와서 말할 때까지 시도하지 않았다는 것을 알지 못하고 게임에 넣을 수있었습니다. 이 멋진 일에! " 그리고 그것은 분명히 대단했기 때문에 그것에 대한 더 강력한 지원을 추가하게되어 기쁩니다.


3

TL : DR-수백 또는 수천 개의 능력을 목록 / 배열에 채우려 고 할 때마다 액션이있을 때마다 액션이 존재하는지 여부와 캐릭터가 있는지 확인하기 위해 반복합니다. 그것을 수행 한 다음 아래를 읽으십시오.

그렇지 않다면 걱정하지 마십시오.
6 문자 / 문자 유형과 30 가지 능력에 대해 이야기하는 경우 복잡성을 관리하는 오버 헤드는 실제로 더미에 모든 것을 덤프하는 것보다 더 많은 코드와 처리가 필요할 수 있기 때문에 실제로는 중요하지 않습니다. 정렬 중 ...

그렇기 때문에 @eBusiness는 이벤트 디스패치 중에 성능 문제가 발생하지 않을 것이라고 제안하는 이유입니다. 정말로 열심히 노력하지 않는 한 3-의 위치를 ​​바꾸는 것에 비해 많은 작업이 없기 때문에 화면에 백만 개의 정점 등

또한 이것은 솔루션 이 아니라 유사한 문제의 더 큰 세트를 관리 하기 위한 솔루션 입니다 ...

그러나...

그것은 모두 게임을 얼마나 크게하는지, 얼마나 많은 캐릭터가 같은 기술을 공유하는지, 얼마나 많은 다른 캐릭터 / 다른 기술이 있는지에 달려 있습니다.

기술이 캐릭터의 구성 요소가되지만 캐릭터가 참가하거나 컨트롤을 떠나거나 (또는 ​​녹아웃 등) 명령 인터페이스에서 등록 / 등록 취소하는 것은 핫키와 커맨드 카드.

나는 Unity의 스크립팅에 대해 거의 경험이 없었지만 언어로서 JavaScript에 매우 익숙합니다.
그들이 그것을 허용한다면, 그 목록이 간단한 객체가 아닌 이유는 무엇입니까?

// Command interface wraps this
var registered_abilities = {},

    register = function (name, callback) {
        registered_abilities[name] = callback;
    },
    unregister = function (name) {
        registered_abilities[name] = null;
    },

    call = function (name,/*arr/undef*/params) {
        var callback = registered_abilities[name];
        if (callback) { callback(params); }
    },

    public_interface = {
        register : register,
        unregister : unregister,
        call : call
    };

return public_interface;

그리고 그것은 다음과 같이 사용될 수 있습니다 :

var command_card = new CommandInterface();

// one-time setup
system.listen("register-ability",   command_card.register  );
system.listen("unregister-ability", command_card.unregister);
system.listen("use-action",         command_card.call      );

// init characters
var dave = new PlayerCharacter("Dave"); // Character Factory pulls out Dave + dependencies
dave.init();

Dave (). init 함수는 다음과 같습니다.

// Inside of Dave class
init = function () {
    // other instance-level stuff ...

    system.notify("register-ability", "repair",  this.Repair );
    system.notify("register-ability", "science", this.Science);
},

die = function () {
    // other clean-up stuff ...

    system.notify("unregister-ability", "repair" );
    system.notify("unregister-ability", "science");
},

resurrect = function () { /* same idea as init */ };

Dave보다 많은 사람이 .Repair()있지만 Dave가 하나만 있다는 것을 보장 할 수 있으면 다음으로 변경하십시오.system.notify("register-ability", "dave:repair", this.Repair);

그리고를 사용하여 기술을 호출 system.notify("use-action", "dave:repair");

사용중인 목록이 어떤지 잘 모르겠습니다. (UnityScript 유형 시스템 및 컴파일 후 진행 상황 측면에서).

아마도 수백 가지의 기술을 가지고 있다면 (현재 사용 가능한 문자를 기반으로 등록 및 등록 취소 대신) 목록에 채우는 것, 전체 JS 배열을 반복하는 것, 수행하려는 작업의 이름과 일치하는 클래스 / 객체의 속성을 확인하기 위해 수행하는 작업 인 경우)보다 성능이 떨어집니다.

보다 최적화 된 구조가 있다면 이것보다 성능이 우수 할 것입니다.

그러나 두 경우 모두 이제는 자신의 행동을 제어하는 ​​캐릭터가 있으며 (한 단계 더 나아가서 원하는 경우 구성 요소 / 엔티티로 만듭니다), 최소한의 반복이 필요한 제어 시스템이 있습니다 (단지 이름별로 테이블 조회 수행).

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