- 补充游戏核心逻辑

- 完成 TodOList P0
    - 添加游戏内控制台便于调试
    - 增加敌人互斥机制提高运算负载
    - 优化互斥算法将游戏性能基准线拉到 1k 敌人
This commit is contained in:
SepComet
2026-02-20 18:08:27 +08:00
parent b4bc8b0445
commit 3742110e9a
196 changed files with 4868 additions and 31326 deletions
+43 -19
View File
@@ -11,11 +11,12 @@ using System.Runtime.InteropServices;
using Definition.DataStruct;
using Definition.Enum;
using Entity;
using Entity.Weapon;
using UnityEngine;
using UnityGameFramework.Runtime;
using Random = UnityEngine.Random;
namespace Game.Utility
namespace CustomUtility
{
/// <summary>
/// AI 工具类。
@@ -134,6 +135,29 @@ namespace Game.Utility
return (toTransform.position - fromTransform.position).sqrMagnitude;
}
/// <summary>
/// 获取实体间在 XZ 平面的距离(忽略 Y 轴)。
/// </summary>
/// <returns>实体间在 XZ 平面的距离。</returns>
public static float GetDistanceXZ(EntityBase fromEntity, EntityBase toEntity)
{
return Mathf.Sqrt(GetSqrMagnitudeXZ(fromEntity, toEntity));
}
/// <summary>
/// 获取实体间在 XZ 平面的平方距离(忽略 Y 轴)。
/// </summary>
/// <returns>实体间在 XZ 平面的平方距离。</returns>
public static float GetSqrMagnitudeXZ(EntityBase fromEntity, EntityBase toEntity)
{
Transform fromTransform = fromEntity.CachedTransform;
Transform toTransform = toEntity.CachedTransform;
Vector3 delta = toTransform.position - fromTransform.position;
delta.y = 0f;
return delta.sqrMagnitude;
}
public static void PerformCollision(TargetableObject entity, EntityBase other)
{
if (entity == null || other == null)
@@ -159,23 +183,23 @@ namespace Game.Utility
// return;
// }
Bullet bullet = other as Bullet;
if (bullet != null)
{
ImpactData entityImpactData = entity.GetImpactData();
ImpactData bulletImpactData = bullet.GetImpactData();
if (GetRelation(entityImpactData.Camp, bulletImpactData.Camp) == RelationType.Friendly)
{
return;
}
int entityDamageHP = CalcDamageHP(bulletImpactData.AttackBase, bulletImpactData.AttackStat,
entityImpactData.DefenseStat, entityImpactData.DodgeStat);
entity.ApplyDamage(bullet, entityDamageHP);
GameEntry.Entity.HideEntity(bullet);
return;
}
// Bullet bullet = other as Bullet;
// if (bullet != null)
// {
// ImpactData entityImpactData = entity.GetImpactData();
// ImpactData bulletImpactData = bullet.GetImpactData();
// if (GetRelation(entityImpactData.Camp, bulletImpactData.Camp) == RelationType.Friendly)
// {
// return;
// }
//
// int entityDamageHP = CalcDamageHP(bulletImpactData.AttackBase, bulletImpactData.AttackStat,
// entityImpactData.DefenseStat, entityImpactData.DodgeStat);
//
// entity.ApplyDamage(bullet, entityDamageHP);
// GameEntry.Entity.HideEntity(bullet);
// return;
// }
WeaponBase weapon = other as WeaponBase;
if (weapon != null)
@@ -240,4 +264,4 @@ namespace Game.Utility
public CampType Second => m_Second;
}
}
}
}
@@ -1,6 +1,6 @@
using Text = GameFramework.Utility.Text;
namespace Game.Utility
namespace CustomUtility
{
public static class AssetUtility
{
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ab0211bce76618a4b9bd9bd8d2a95fb6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,78 @@
using System.Collections.Generic;
using Components;
using UnityEngine;
namespace CustomUtility
{
public static class EnemySeparationSolverProvider
{
private struct Registration
{
public Transform Transform;
public float BodyRadius;
}
private static IEnemySeparationSolver _current = new GridBucketEnemySeparationSolver();
private static readonly Dictionary<MovementComponent, Registration> Registrations = new();
public static IEnemySeparationSolver Current => _current;
public static string CurrentSolverName => _current.GetType().Name;
public static void SetSolver(IEnemySeparationSolver solver)
{
if (solver == null) return;
_current = solver;
ReRegisterAll();
}
public static void UseGridBucketSolver(float cellSize = 1f)
{
SetSolver(new GridBucketEnemySeparationSolver(cellSize));
}
public static void UseNaiveSolver()
{
SetSolver(new NaiveEnemySeparationSolver());
}
public static void Register(MovementComponent mover, Transform transform, float bodyRadius)
{
if (mover == null || transform == null) return;
var registration = new Registration
{
Transform = transform,
BodyRadius = bodyRadius
};
Registrations[mover] = registration;
_current.Register(mover, transform, bodyRadius);
}
public static void Unregister(MovementComponent mover)
{
if (mover == null) return;
_current.Unregister(mover);
Registrations.Remove(mover);
}
public static Vector3 Resolve(MovementComponent mover, Vector3 desiredPosition, Vector3 fallbackDirection,
int iterations)
{
return _current.Resolve(mover, desiredPosition, fallbackDirection, iterations);
}
private static void ReRegisterAll()
{
foreach (var pair in Registrations)
{
MovementComponent mover = pair.Key;
Registration registration = pair.Value;
if (mover == null || registration.Transform == null) continue;
_current.Register(mover, registration.Transform, registration.BodyRadius);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3cf44095cd7c76043a8e8a44dc5a0888
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,223 @@
using Components;
using UnityEngine;
namespace CustomUtility
{
public sealed class GridBucketEnemySeparationSolver : IEnemySeparationSolver
{
private sealed class Agent
{
public Transform Transform;
public float Radius;
public Vector3 Position;
public int CellX;
public int CellZ;
}
private readonly System.Collections.Generic.Dictionary<MovementComponent, Agent> _agents = new();
private readonly System.Collections.Generic.Dictionary<long, System.Collections.Generic.List<MovementComponent>>
_buckets = new();
private readonly System.Collections.Generic.List<MovementComponent> _recycle = new();
private readonly float _cellSize;
private int _snapshotFrame = -1;
private float _maxRadius = 0.45f;
public GridBucketEnemySeparationSolver(float cellSize = 1f)
{
_cellSize = Mathf.Max(0.1f, cellSize);
}
public void Register(MovementComponent mover, Transform transform, float bodyRadius)
{
if (mover == null || transform == null) return;
if (!_agents.TryGetValue(mover, out var agent))
{
agent = new Agent();
_agents.Add(mover, agent);
}
agent.Transform = transform;
agent.Radius = Mathf.Max(0.01f, bodyRadius);
if (agent.Radius > _maxRadius)
{
_maxRadius = agent.Radius;
}
_snapshotFrame = -1;
}
public void Unregister(MovementComponent mover)
{
if (mover == null) return;
if (!_agents.TryGetValue(mover, out var agent)) return;
RemoveFromBucket(mover, agent.CellX, agent.CellZ);
_agents.Remove(mover);
RecalculateMaxRadius();
_snapshotFrame = -1;
}
public Vector3 Resolve(MovementComponent mover, Vector3 desiredPosition, Vector3 fallbackDirection,
int iterations)
{
if (mover == null) return desiredPosition;
if (!_agents.TryGetValue(mover, out var self)) return desiredPosition;
EnsureSnapshot();
Vector3 candidate = desiredPosition;
candidate.y = 0f;
int effectiveIterations = Mathf.Max(1, iterations);
int queryRange = Mathf.Max(1, Mathf.CeilToInt((self.Radius + _maxRadius) / _cellSize));
Vector3 fallback = fallbackDirection.sqrMagnitude > 0.0001f ? fallbackDirection.normalized : Vector3.right;
fallback.y = 0f;
for (int iter = 0; iter < effectiveIterations; iter++)
{
int cellX = ToCell(candidate.x);
int cellZ = ToCell(candidate.z);
for (int dx = -queryRange; dx <= queryRange; dx++)
{
for (int dz = -queryRange; dz <= queryRange; dz++)
{
if (!_buckets.TryGetValue(CellKey(cellX + dx, cellZ + dz), out var bucket)) continue;
for (int i = 0; i < bucket.Count; i++)
{
MovementComponent otherMover = bucket[i];
if (otherMover == mover) continue;
if (!_agents.TryGetValue(otherMover, out var other)) continue;
Vector3 toSelf = candidate - other.Position;
float minDistance = self.Radius + other.Radius;
float minDistanceSq = minDistance * minDistance;
float sqrDistance = toSelf.sqrMagnitude;
if (sqrDistance <= Mathf.Epsilon)
{
candidate += fallback * (self.Radius * 0.25f);
continue;
}
if (sqrDistance >= minDistanceSq) continue;
float distance = Mathf.Sqrt(sqrDistance);
float penetration = minDistance - distance;
candidate += (toSelf / distance) * penetration;
}
}
}
}
SyncAgentPosition(mover, self, candidate);
candidate.y = desiredPosition.y;
return candidate;
}
private void EnsureSnapshot()
{
int frame = Time.frameCount;
if (_snapshotFrame == frame) return;
_snapshotFrame = frame;
_buckets.Clear();
_recycle.Clear();
foreach (var pair in _agents)
{
MovementComponent mover = pair.Key;
Agent agent = pair.Value;
if (mover == null || agent.Transform == null)
{
_recycle.Add(mover);
continue;
}
Vector3 position = agent.Transform.position;
position.y = 0f;
agent.Position = position;
agent.CellX = ToCell(position.x);
agent.CellZ = ToCell(position.z);
AddToBucket(mover, agent.CellX, agent.CellZ);
}
for (int i = 0; i < _recycle.Count; i++)
{
_agents.Remove(_recycle[i]);
}
}
private void SyncAgentPosition(MovementComponent mover, Agent agent, Vector3 position)
{
int newCellX = ToCell(position.x);
int newCellZ = ToCell(position.z);
if (agent.CellX != newCellX || agent.CellZ != newCellZ)
{
RemoveFromBucket(mover, agent.CellX, agent.CellZ);
AddToBucket(mover, newCellX, newCellZ);
agent.CellX = newCellX;
agent.CellZ = newCellZ;
}
agent.Position = position;
}
private void AddToBucket(MovementComponent mover, int cellX, int cellZ)
{
long key = CellKey(cellX, cellZ);
if (!_buckets.TryGetValue(key, out var list))
{
list = new System.Collections.Generic.List<MovementComponent>(8);
_buckets.Add(key, list);
}
list.Add(mover);
}
private void RemoveFromBucket(MovementComponent mover, int cellX, int cellZ)
{
long key = CellKey(cellX, cellZ);
if (!_buckets.TryGetValue(key, out var list)) return;
list.Remove(mover);
if (list.Count == 0)
{
_buckets.Remove(key);
}
}
private void RecalculateMaxRadius()
{
float max = 0.01f;
foreach (var pair in _agents)
{
if (pair.Value.Radius > max)
{
max = pair.Value.Radius;
}
}
_maxRadius = max;
}
private int ToCell(float value)
{
return Mathf.FloorToInt(value / _cellSize);
}
private static long CellKey(int x, int z)
{
return ((long)x << 32) ^ (uint)z;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7c10dca24b508f4fa6726eae7ac2fb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,12 @@
using Components;
using UnityEngine;
namespace CustomUtility
{
public interface IEnemySeparationSolver
{
void Register(MovementComponent mover, Transform transform, float bodyRadius);
void Unregister(MovementComponent mover);
Vector3 Resolve(MovementComponent mover, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e3960124c8fe4304493659a13e5a9439
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,90 @@
using Components;
using UnityEngine;
namespace CustomUtility
{
public sealed class NaiveEnemySeparationSolver : IEnemySeparationSolver
{
private sealed class Agent
{
public Transform Transform;
public float Radius;
}
private readonly System.Collections.Generic.Dictionary<MovementComponent, Agent> _agents = new();
private readonly System.Collections.Generic.List<MovementComponent> _agentKeys = new();
public void Register(MovementComponent mover, Transform transform, float bodyRadius)
{
if (mover == null || transform == null) return;
if (!_agents.TryGetValue(mover, out var agent))
{
agent = new Agent();
_agents.Add(mover, agent);
}
agent.Transform = transform;
agent.Radius = Mathf.Max(0.01f, bodyRadius);
}
public void Unregister(MovementComponent mover)
{
if (mover == null) return;
_agents.Remove(mover);
}
public Vector3 Resolve(MovementComponent mover, Vector3 desiredPosition, Vector3 fallbackDirection, int iterations)
{
if (mover == null) return desiredPosition;
if (!_agents.TryGetValue(mover, out var self)) return desiredPosition;
Vector3 candidate = desiredPosition;
candidate.y = 0f;
Vector3 fallback = fallbackDirection.sqrMagnitude > 0.0001f ? fallbackDirection.normalized : Vector3.right;
fallback.y = 0f;
_agentKeys.Clear();
foreach (var pair in _agents)
{
_agentKeys.Add(pair.Key);
}
int effectiveIterations = Mathf.Max(1, iterations);
for (int iter = 0; iter < effectiveIterations; iter++)
{
for (int i = 0; i < _agentKeys.Count; i++)
{
MovementComponent otherMover = _agentKeys[i];
if (otherMover == mover) continue;
if (!_agents.TryGetValue(otherMover, out var other)) continue;
if (other.Transform == null) continue;
Vector3 otherPosition = other.Transform.position;
otherPosition.y = 0f;
Vector3 toSelf = candidate - otherPosition;
float minDistance = self.Radius + other.Radius;
float minDistanceSq = minDistance * minDistance;
float sqrDistance = toSelf.sqrMagnitude;
if (sqrDistance <= Mathf.Epsilon)
{
candidate += fallback * (self.Radius * 0.25f);
continue;
}
if (sqrDistance >= minDistanceSq) continue;
float distance = Mathf.Sqrt(sqrDistance);
float penetration = minDistance - distance;
candidate += (toSelf / distance) * penetration;
}
}
candidate.y = desiredPosition.y;
return candidate;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec8ec1013900437498da4613f680a898
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,7 +1,7 @@
using System.Collections.Generic;
using UnityGameFramework.Runtime;
namespace StarForce
namespace CustomUtility
{
public static class EnumUtility<T> where T : struct, System.Enum
{
@@ -1,13 +1,21 @@
using System.Collections.Generic;
using System.Text;
using DataTable;
using Definition.DataStruct;
using Entity;
using Entity.EntityData;
using Entity.Weapon;
using UnityGameFramework.Runtime;
namespace Game.Utility
namespace CustomUtility
{
public static class ItemDescUtility
{
private static readonly Dictionary<string, string> _paramsDict = new()
{
{"hitradius", "伤害范围"},
{"sectorangle", "攻击角度"}
};
public static string CreatePropDescription(StatModifier[] modifiers)
{
if (modifiers == null || modifiers.Length == 0)
@@ -32,7 +40,8 @@ namespace Game.Utility
return null;
}
return CreateWeaponDescription(drWeapon.Attack, drWeapon.Cooldown, drWeapon.AttackRange, drWeapon.Modifiers);
return CreateWeaponDescription(drWeapon.Attack, drWeapon.Cooldown, drWeapon.AttackRange, drWeapon.Modifiers,
drWeapon.Pramas);
}
public static string CreateWeaponDescription(WeaponBase weapon)
@@ -44,7 +53,7 @@ namespace Game.Utility
return CreateWeaponDescription(weapon.WeaponData);
}
public static string CreateWeaponDescription(WeaponData weaponData)
{
if (weaponData == null)
@@ -53,7 +62,7 @@ namespace Game.Utility
}
return CreateWeaponDescription(weaponData.Attack, weaponData.Cooldown,
weaponData.AttackRange, weaponData.Modifiers);
weaponData.AttackRange, weaponData.Modifiers, weaponData.Params);
}
public static string CreatePropDescription(PropItem prop)
@@ -67,12 +76,12 @@ namespace Game.Utility
}
private static string CreateWeaponDescription(int attack, float cooldown, float attackRange,
StatModifier[] modifiers)
StatModifier[] modifiers, Dictionary<string, string> @params)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine($"Attack: {attack}");
sb.AppendLine($"Cooldown: {cooldown}");
sb.AppendLine($"Range: {attackRange}");
sb.AppendLine($"攻击: {attack}");
sb.AppendLine($"冷却: {cooldown}");
sb.AppendLine($"范围: {attackRange}");
string modifiersDesc = CreatePropDescription(modifiers);
if (!string.IsNullOrEmpty(modifiersDesc))
@@ -80,7 +89,17 @@ namespace Game.Utility
sb.Append(modifiersDesc);
}
foreach (var kvp in @params)
{
if (!_paramsDict.TryGetValue(kvp.Key, out string value))
{
value = kvp.Key;
Log.Warning($"{kvp.Key} is not defined.");
}
sb.AppendLine($"{value}: {kvp.Value}");
}
return sb.ToString();
}
}
}
}
@@ -1,7 +1,7 @@
using System;
using Newtonsoft.Json;
namespace Game.Utility
namespace CustomUtility
{
/// <summary>
/// Newtonsoft.Json 函数集辅助器。
@@ -3,7 +3,7 @@ using Definition.Enum;
using GameFramework.DataTable;
using UnityEngine;
namespace Game.Utility
namespace CustomUtility
{
public static class RarityUtility
{