简体   繁体   English

统一。 如何在 ScriptableObject 的代码中存储通用数据以供使用(不同的 NPC 类型)

[英]Unity. How to store generic data for usage in ScriptableObject's code (different NPC types)

I have a bunch of different kind of NPCs in my game and of course they logically similar, they have health, they have vision, they can navigate using agent and stuff.我的游戏中有很多不同类型的 NPC,当然它们在逻辑上相似,它们有健康、有远见、可以使用代理和其他东西导航。

But each NPC type has it's own custom behavior with states, actions, decisions and hooks.但是每种 NPC 类型都有自己的自定义行为,包括状态、动作、决策和钩子。 And those scripts require various specific data like coroutines running, target altitude or current leaping direction.这些脚本需要各种特定的数据,如协程运行、目标高度或当前跳跃方向。

And I have to store it or have it on NPC mono behavior, so it is accessible inside state's scripts (they are scriptable objects called from NPC mono behavior)而且我必须将其存储或保存在 NPC 单声道行为中,因此可以在状态脚本中访问它(它们是从 NPC 单声道行为调用的可编写脚本的对象)

Right now what I do is specifying array for each data type and count of it that I assign on NPC prefab.现在我所做的是为每种数据类型指定数组,并在 NPC 预制件上分配它的计数。 And it feels wrong...而且感觉不对...

在此处输入图像描述

public class Npc : MonoBehaviour
{
public static Dictionary<int, Npc> npcs = new Dictionary<int, Npc>();

public int npcId;
public NpcType type;

public Transform shootOrigin;
public Transform head;

public float maxHealth = 50f;
public float visionRange = 15;
public float visionAngle = 60;
public float headAngle = 120;
public float movementSpeed = 4.5f;

public int indexedActionsCount = 0;
[HideInInspector] public float[] lastActTimeIndexed;
[HideInInspector] public bool[] wasActionCompletedIndexed;

public int indexedVector3DataCount = 0;
[HideInInspector] public Vector3[] vector3DataIndexed;

public int indexedFloatDataCount = 0;
[HideInInspector] public float[] floatDataIndexed;

public int indexedBoolDataCount = 0;
[HideInInspector] public bool[] boolDataIndexed;

public int indexedCoroutineDataCount = 0;
[HideInInspector] public IEnumerator[] coroutineDataIndexed;

public NpcState currentState;
public NpcState remainState;

public float Health { get; private set; }

[HideInInspector] public NavMeshAgent agent;

public static int decisionUpdatesPerSecond = 2; // Check for decisions in 2FPS
public static int actionUpdatesPerSecond = 5; // Act in 5FPS
public static int reportUpdatesPerSecond = 15; // Report in 15FPS

private static int nextNpcId = 10000;


public void Awake()
{
    agent = GetComponent<NavMeshAgent>();
}

public void Start()
{
    npcId = nextNpcId;
    nextNpcId++;
    npcs.Add(npcId, this);

    Health = maxHealth;
    agent.speed = movementSpeed;

    lastActTimeIndexed = new float[indexedActionsCount];
    wasActionCompletedIndexed = new bool[indexedActionsCount];

    floatDataIndexed = new float[indexedFloatDataCount];
    boolDataIndexed = new bool[indexedBoolDataCount];
    vector3DataIndexed = new Vector3[indexedVector3DataCount];
    coroutineDataIndexed = new IEnumerator[indexedCoroutineDataCount];

    ServerSend.SpawnNpc(npcId, type, transform.position);

    InvokeRepeating("GetTarget", 1.0f, 1.0f);
    InvokeRepeating("UpdateDecisions", 0.0f, 1.0f / decisionUpdatesPerSecond);
    InvokeRepeating("UpdateActions", 0.0f, 1.0f / actionUpdatesPerSecond);
    InvokeRepeating("SendUpdates", 0.0f, 1.0f / reportUpdatesPerSecond);

    OnEnterState();
}

public void TakeDamage(float _damage)
{

}

public bool GoTo(Vector3 location)
{

}

public void TransitionToState(NpcState nextState)
{
    OnExitState();
    currentState = nextState;
    OnEnterState();
}

public void StartCoroutineOnNpc(IEnumerator routine)
{
    StartCoroutine(routine);
}

public void StopCoroutineOnNpc(IEnumerator routine)
{
    StopCoroutine(routine);
}

private void OnEnterState()
{
    var hooks = currentState.onEnterHooks;
    for (int i = 0; i < hooks.Length; i++)
    {
        hooks[i].Apply(this);
    }
    stateTimeOnEnter = Time.time;
    wasActionCompleted = false;
}

private void OnExitState()
{
    var hooks = currentState.onExitHooks;
    for (int i = 0; i < hooks.Length; i++)
    {
        hooks[i].Apply(this);
    }
}

private void UpdateDecisions()
{
    currentState.UpdateDecisions(this);
}

private void UpdateActions()
{
    currentState.UpdateState(this);
}

private void SendUpdates()
{
    ServerSend.NpcState(this);
}
}

In JavaScript world I would just have 1 array or object and put any data this particular NPC needs to it.在 JavaScript 世界中,我将只有 1 个数组或对象,并将该特定 NPC 需要的任何数据放入其中。 But in C# I need a strongly typed place to put data for each data type my scripts could require.但是在 C# 中,我需要一个强类型的地方来放置我的脚本可能需要的每种数据类型的数据。

Example of data usage in script:脚本中的数据使用示例:

在此处输入图像描述

I don't think having so many arrays and counters on MonoBehavior is a good idea, especially that there may be a lot of NPCs on scene.我不认为在 MonoBehavior 上有这么多数组和计数器是一个好主意,尤其是现场可能有很多 NPC。 Any advice on building better storage while maintaining script flexibility?在保持脚本灵活性的同时构建更好的存储有什么建议吗?

Clarification: All the behavior logic is controlled by flexible ScriptableObject states.澄清:所有行为逻辑都由灵活的ScriptableObject状态控制。 The problem is these objects cannot store any runtime data, but they have access to my Npc MonoBehavior (component) instance.问题是这些对象不能存储任何运行时数据,但它们可以访问我的 Npc MonoBehavior (组件)实例。

在此处输入图像描述

Initial code for this approach came from Unity tutorial这种方法的初始代码来自Unity 教程

Let me explain the structure I ended up using for the case I described:让我解释一下我最终在我描述的案例中使用的结构:

If particular NPC requires some specific data for its behavior I will add another component (in this example leaper NPC needs to store data for leaping behavior)如果特定的 NPC 需要一些特定的行为数据,我将添加另一个组件(在此示例中,跳跃者 NPC 需要存储跳跃行为的数据)

在此处输入图像描述

This data is defined in interface (it's important, because 1 NPC may implement multiple interfaces [several reused behaviors])这些数据在接口中定义(这很重要,因为1个NPC可能实现多个接口[几个重用行为])

public interface ILeaperData
{
    public Vector3 leapTarget { get; set; }
    public Vector3 initialPosition { get; set; }
    public bool startedLeap { get; set; }
    public float lastLeapTime { get; set; }
}

And then this NPC type will have component that implements this interface (and 1 more in this example)然后这个 NPC 类型将有实现这个接口的组件(在这个例子中还有 1 个)

public class LeaperData : NpcData, ILeaperData, ICompletedActionData
{
    public Vector3 leapTarget { get; set; }
    public Vector3 initialPosition { get; set; }
    public bool startedLeap { get; set; }
    public float lastLeapTime { get; set; }

    public bool wasActionCompleted { get; set; }
}

That way I can reuse data interfaces when the same behavior is used on other NPC types.这样,当在其他 NPC 类型上使用相同的行为时,我可以重用数据接口。

Example of how it is used in ScriptableObject logic:如何在 ScriptableObject 逻辑中使用它的示例:

[CreateAssetMenu(menuName = "AI/Decisions/CanLeap")]
public class CanLeapDecision : NpcDecision
{
    public int nextAngle = 45;
    public float radius = 4;

    public override bool Decide(Npc npc)
    {
        if (npc.target)
        {
            var dir = (npc.transform.position - npc.target.position).normalized;
            var dir2 = new Vector2(dir.x, dir.z).normalized * radius;
            var dir3 = new Vector3(dir2.x, dir.y, dir2.y);


            if (NavMesh.SamplePosition(RotateAroundPoint(npc.target.position + dir3, npc.target.position, Quaternion.Euler(0, nextAngle * ((Random.value > 0.5f) ? 1 : -1), 0)), out var hit, 3.5f, 1))
            {
                var path = new NavMeshPath();
                npc.agent.CalculatePath(hit.position, path);

                if (path.corners.Length == 2 && path.status == NavMeshPathStatus.PathComplete)
                {
                    ((ILeaperData)npc.npcData).leapTarget = hit.position;
                    ((ILeaperData)npc.npcData).initialPosition = npc.transform.position;
                    ((ILeaperData)npc.npcData).startedLeap = false;
                    return true;
                }
            }
        }
        return false;
    }

    private Vector3 RotateAroundPoint(Vector3 point, Vector3 pivot, Quaternion angle)
    {
        var finalPos = point - pivot;
        //Center the point around the origin
        finalPos = angle * finalPos;
        //Rotate the point.

        finalPos += pivot;
        //Move the point back to its original offset. 
        return finalPos;
    }
}

You can see the cast to (ILeaperData) where I need the data stored on this NPC instance.您可以看到转换为 (ILeaperData) 我需要存储在此 NPC 实例上的数据。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM