1

我的游戏中有很多不同类型的 NPC,当然它们在逻辑上相似,它们有健康、有远见、可以使用代理和其他东西导航。

但是每种 NPC 类型都有自己的自定义行为,包括状态、动作、决策和钩子。这些脚本需要各种特定的数据,如协程运行、目标高度或当前跳跃方向。

而且我必须将其存储或保存在 NPC 单声道行为中,因此可以在状态脚本中访问它(它们是从 NPC 单声道行为调用的可编写脚本的对象)

现在我所做的是为每种数据类型指定数组,并在 NPC 预制件上分配它的计数。而且感觉不对...

在此处输入图像描述

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);
}
}

在 JavaScript 世界中,我将只有 1 个数组或对象,并将该特定 NPC 需要的任何数据放入其中。但是在 C# 中,我需要一个强类型的地方来放置我的脚本可能需要的每种数据类型的数据。

脚本中的数据使用示例:

在此处输入图像描述

我不认为在 MonoBehavior 上有这么多数组和计数器是一个好主意,尤其是现场可能有很多 NPC。在保持脚本灵活性的同时构建更好的存储有什么建议吗?

澄清: 所有行为逻辑都由灵活的ScriptableObject状态控制。问题是这些对象不能存储任何运行时数据,但它们可以访问我的 Npc MonoBehavior(组件)实例。

在此处输入图像描述

这种方法的初始代码来自Unity 教程

4

1 回答 1

0

让我解释一下我最终在我描述的案例中使用的结构:

如果特定的 NPC 需要一些特定的行为数据,我将添加另一个组件(在此示例中,跳跃者 NPC 需要存储跳跃行为的数据)

在此处输入图像描述

这些数据在接口中定义(这很重要,因为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; }
}

然后这个 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; }
}

这样,当在其他 NPC 类型上使用相同的行为时,我可以重用数据接口。

如何在 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;
    }
}

您可以看到转换为 (ILeaperData) 我需要存储在此 NPC 实例上的数据。

于 2022-01-14T22:25:04.557 回答