- 补充游戏核心逻辑

- 完成 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
@@ -1,71 +0,0 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using System;
using Definition.Enum;
using StarForce;
using UnityEngine;
namespace Entity.EntityData
{
[Serializable]
public class BulletData : EntityDataBase
{
[SerializeField]
private int m_OwnerId = 0;
[SerializeField]
private CampType m_OwnerCamp = CampType.Unknown;
[SerializeField]
private int m_Attack = 0;
[SerializeField]
private float m_Speed = 0f;
public BulletData(int entityId, int typeId, int ownerId, CampType ownerCamp, int attack, float speed)
: base(entityId, typeId)
{
m_OwnerId = ownerId;
m_OwnerCamp = ownerCamp;
m_Attack = attack;
m_Speed = speed;
}
public int OwnerId
{
get
{
return m_OwnerId;
}
}
public CampType OwnerCamp
{
get
{
return m_OwnerCamp;
}
}
public int Attack
{
get
{
return m_Attack;
}
}
public float Speed
{
get
{
return m_Speed;
}
}
}
}
@@ -8,50 +8,51 @@ namespace Entity.EntityData
[Serializable]
public class EnemyData : TargetableObjectData
{
[SerializeField] private EnemyType _enemyType;
[SerializeField] private int _entityTypeId;
[SerializeField] private float _speedBase = 0;
[SerializeField] private int _dropCoin = 0;
[SerializeField] private int _dropExp = 0;
[SerializeField] private float _dropPercent = 0;
public EnemyData(int entityId, int typeId, int level) : base(
entityId, typeId, CampType.Enemy)
public EnemyData(int entityId, EnemyType enemyType, int level) : base(
entityId, (int)enemyType, CampType.Enemy)
{
DREnemy enemyRow = GameEntry.DataTable.GetDataTableRow<DREnemy>(typeId);
DREnemy enemyRow = GameEntry.DataTable.GetDataTableRow<DREnemy>((int)enemyType);
if (enemyRow == null)
{
throw new Exception($"Enemy data table row is missing, EnemyType='{enemyType}'.");
}
int effectiveLevel = Mathf.Max(1, level);
_enemyType = enemyType;
_entityTypeId = enemyRow.EntityTypeId;
MaxHealthBase = enemyRow.MaxHealth + enemyRow.HpAddPerLevel * (effectiveLevel - 1);
SpeedBase = enemyRow.Speed;
DropCoin = enemyRow.DropCoin;
DropExp = enemyRow.DropExp;
DropPercent = enemyRow.DropPercent;
_speedBase = enemyRow.Speed;
_dropCoin = enemyRow.DropCoin;
_dropExp = enemyRow.DropExp;
_dropPercent = enemyRow.DropPercent;
}
public EnemyType EnemyType => _enemyType;
public int EntityTypeId => _entityTypeId;
public override int MaxHealthBase { get; }
public float SpeedBase
{
get => _speedBase;
set => _speedBase = value;
}
public float SpeedBase => _speedBase;
public int DropCoin
{
get => _dropCoin;
set => _dropCoin = value;
}
public int DropCoin => _dropCoin;
public int DropExp
{
get => _dropExp;
set => _dropExp = value;
}
public int DropExp => _dropExp;
public float DropPercent
{
get => _dropPercent;
set => _dropPercent = value;
}
public float DropPercent => _dropPercent;
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using DataTable;
using Definition.DataStruct;
using Definition.Enum;
@@ -10,10 +11,31 @@ namespace Entity.EntityData
{
private DRWeapon _drWeapon;
public WeaponData(int entityId, int typeId, int ownerId, CampType ownerCamp)
: base(entityId, typeId, ownerId, ownerCamp)
private int _entityTypeId = 0;
public WeaponData(int entityId, WeaponType weaponType, int ownerId, CampType ownerCamp)
: base(entityId, (int)weaponType, ownerId, ownerCamp)
{
_drWeapon = GameEntry.DataTable.GetDataTableRow<DRWeapon>(TypeId);
_drWeapon = GameEntry.DataTable.GetDataTableRow<DRWeapon>((int)weaponType);
if (_drWeapon == null)
{
throw new Exception($"Weapon data table row is missing, WeaponType='{weaponType}'.");
}
_entityTypeId = _drWeapon.EntityTypeId;
}
public WeaponType WeaponType => (WeaponType)_drWeapon.Id;
public string GetParamsString(string paramsName)
{
if (!Params.TryGetValue(paramsName.ToLower(), out var value))
{
throw new Exception($"Parameter '{paramsName}' not found.");
}
return value;
}
/// <summary>
@@ -21,6 +43,8 @@ namespace Entity.EntityData
/// </summary>
public int Attack => _drWeapon.Attack;
public int EntityTypeId => _entityTypeId;
/// <summary>
/// 武器名称。
/// </summary>
@@ -53,7 +77,7 @@ namespace Entity.EntityData
/// <summary>
/// 额外参数。
/// </summary>
public string Params => _drWeapon.Pramas;
public Dictionary<string, string> Params => _drWeapon.Pramas;
/// <summary>
/// 额外属性。
@@ -0,0 +1,12 @@
using Definition.Enum;
namespace Entity.EntityData
{
public class WeaponHandgunData : WeaponData
{
public WeaponHandgunData(int entityId, int ownerId, CampType ownerCamp)
: base(entityId, WeaponType.WeaponHandgun, ownerId, ownerCamp)
{
}
}
}
@@ -1,8 +1,7 @@
fileFormatVersion: 2
guid: 16371cc6e2112964d8b5bf7400c862cd
timeCreated: 1528026152
licenseType: Pro
guid: 574d105bf89bf7546a682ed407708034
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
@@ -4,7 +4,7 @@ namespace Entity.EntityData
{
public class WeaponKnifeData : WeaponData
{
public WeaponKnifeData(int entityId, int typeId, int ownerId, CampType ownerCamp) : base(entityId, typeId,
public WeaponKnifeData(int entityId, int ownerId, CampType ownerCamp) : base(entityId, WeaponType.WeaponKnife,
ownerId, ownerCamp)
{
}
@@ -0,0 +1,12 @@
using Definition.Enum;
namespace Entity.EntityData
{
public class WeaponSlashData : WeaponData
{
public WeaponSlashData(int entityId, int ownerId, CampType ownerCamp)
: base(entityId, WeaponType.WeaponSlash, ownerId, ownerCamp)
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4aa6473525dc4c62aeee02ff562ec453
timeCreated: 1771484661
@@ -1,35 +1,25 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using System;
using System;
using System.Collections.Generic;
using Definition;
using Entity.EntityData;
using UnityGameFramework.Runtime;
using DataTable;
using Definition.Enum;
using Game.Utility;
using CustomUtility;
namespace Entity
{
public static class EntityExtension
{
// 关于 EntityId 的约定:
// 0 为无效
// 正值用于和服务器通信的实体(如玩家角色、NPC、怪等,服务器只产生正值)
// 负值用于本地生成的临时实体(如特效、FakeObject等)
private static int s_SerialId = -10;
private const string EntityNamespace = "Entity.";
private static Dictionary<string, Type> _typeDict;
private static readonly Dictionary<string, Type> _typeDict;
private static readonly Dictionary<int, string> _assetNameDict;
static EntityExtension()
{
_typeDict = new Dictionary<string, Type>();
_assetNameDict = new Dictionary<int, string>();
}
public static EntityBase GetGameEntity(this EntityComponent entityComponent, int entityId)
@@ -61,44 +51,63 @@ namespace Entity
public static void ShowEnemy(this EntityComponent entityComponent, EnemyData data)
{
string typeName = EntityNamespace + (EnemyType)data.TypeId;
if (!_typeDict.TryGetValue(typeName, out Type enemyType))
if (data == null)
{
enemyType = Type.GetType(typeName);
if (enemyType == null)
{
Log.Warning("Can not load entity type '{0}'.", typeName);
return;
}
_typeDict.Add(typeName, enemyType);
Log.Warning("Enemy data is invalid.");
return;
}
entityComponent.ShowEntity(enemyType, "Enemy", Constant.AssetPriority.EnemyAsset, data);
var enemyType = TryGetType(data.EnemyType.ToString());
var assetName = TryGetAssetName(data.EntityTypeId);
entityComponent.ShowEntity(
entityId: data.Id,
entityLogicType: enemyType,
entityAssetName: AssetUtility.GetEntityAsset(assetName),
entityGroupName: "Enemy",
priority: Constant.AssetPriority.BulletAsset,
userData: data);
}
public static void ShowWeapon(this EntityComponent entityComponent, WeaponData data)
{
string typeName = EntityNamespace + (WeaponType)data.TypeId;
if (!_typeDict.TryGetValue(typeName, out Type weaponType))
if (data == null)
{
weaponType = Type.GetType(typeName);
if (weaponType == null)
{
Log.Warning("Can not load entity type '{0}'.", typeName);
return;
}
_typeDict.Add(typeName, weaponType);
Log.Warning("Weapon data is invalid.");
return;
}
entityComponent.ShowEntity(weaponType, "Weapon", Constant.AssetPriority.WeaponAsset, data);
var weaponType = TryGetType("Weapon." + data.WeaponType);
var assetName = TryGetAssetName(data.EntityTypeId);
entityComponent.ShowEntity(
entityId: data.Id,
entityLogicType: weaponType,
entityAssetName: AssetUtility.GetEntityAsset(assetName),
entityGroupName: "Weapon",
priority: Constant.AssetPriority.BulletAsset,
userData: data);
}
public static void ShowBullet(this EntityComponent entityComponent, BulletData data)
{
entityComponent.ShowEntity(typeof(Bullet), "Bullet", Constant.AssetPriority.BulletAsset, data);
}
// public static void ShowBullet(this EntityComponent entityComponent, BulletData data)
// {
// if (data == null)
// {
// Log.Warning("Bullet data is invalid.");
// return;
// }
//
// var bulletType = TryGetType(data.BulletType.ToString());
// string assetName = TryGetAssetName(data.EntityTypeId);
//
// entityComponent.ShowEntity(
// entityId: data.Id,
// entityLogicType: bulletType,
// entityAssetName: AssetUtility.GetEntityAsset(assetName),
// entityGroupName: "Bullet",
// priority: Constant.AssetPriority.BulletAsset,
// userData: data);
// }
public static void ShowEffect(this EntityComponent entityComponent, EffectData data)
{
@@ -135,6 +144,42 @@ namespace Entity
priority, data);
}
private static Type TryGetType(string rawTypeName)
{
string typeName = EntityNamespace + rawTypeName;
if (!_typeDict.TryGetValue(typeName, out Type type))
{
type = Type.GetType(typeName);
if (type == null)
{
Log.Warning("Can not load entity type '{0}'.", typeName);
return null;
}
_typeDict.Add(typeName, type);
}
return type;
}
private static string TryGetAssetName(int entityId)
{
if (!_assetNameDict.TryGetValue(entityId, out string assetName))
{
DREntity drEntity = GameEntry.DataTable.GetDataTableRow<DREntity>(entityId);
if (drEntity == null)
{
Log.Warning("Can not load bullet entity id '{0}' from data table.", entityId.ToString());
return null;
}
assetName = drEntity.AssetName;
_assetNameDict.Add(entityId, assetName);
}
return assetName;
}
public static int GenerateSerialId(this EntityComponent entityComponent)
{
return --s_SerialId;
@@ -1,53 +0,0 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
using Definition.DataStruct;
using Entity.EntityData;
using StarForce;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity
{
/// <summary>
/// 子弹类。
/// </summary>
public class Bullet : EntityBase
{
[SerializeField] private BulletData m_BulletData = null;
public ImpactData GetImpactData()
{
//TODO:子弹的 AttackStat 不该为 null
return new ImpactData(m_BulletData.OwnerCamp, m_BulletData.Attack, null);
}
protected override void OnInit(object userData)
{
base.OnInit(userData);
}
protected override void OnShow(object userData)
{
base.OnShow(userData);
m_BulletData = userData as BulletData;
if (m_BulletData == null)
{
Log.Error("Bullet data is invalid.");
return;
}
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
CachedTransform.Translate(Vector3.forward * m_BulletData.Speed * elapseSeconds, Space.World);
}
}
}
@@ -1,6 +1,7 @@
using Components;
using Definition.DataStruct;
using Entity.EntityData;
using Entity.Weapon;
using UnityEngine;
using UnityGameFramework.Runtime;
@@ -39,7 +40,7 @@ namespace Entity
{
_meleeEnemyData = enemyData;
_healthComponent.OnInit(enemyData.MaxHealthBase);
_movementComponent.OnInit(_meleeEnemyData.SpeedBase, this.CachedTransform);
_movementComponent.OnInit(_meleeEnemyData.SpeedBase, this.CachedTransform, null, true);
_movementComponent.SetMove(true);
_attackRangeSquared = _attackRange * _attackRange;
this.CachedTransform.position = enemyData.Position;
@@ -55,6 +56,13 @@ namespace Entity
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
if (_target == null)
{
_movementComponent.SetMove(false);
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
return;
}
float distanceSquared = (this.CachedTransform.position - _target.position).sqrMagnitude;
if (distanceSquared < _attackRangeSquared)
{
@@ -105,6 +113,11 @@ namespace Entity
private Vector3 GetTargetDirection()
{
if (_target == null)
{
return Vector3.zero;
}
return new Vector3(
_target.position.x - this.CachedTransform.position.x,
0f,
@@ -112,4 +125,4 @@ namespace Entity
).normalized;
}
}
}
}
@@ -37,7 +37,7 @@ namespace Entity
{
_remoteEnemyData = enemyData;
_healthComponent.OnInit(enemyData.MaxHealthBase);
_movementComponent.OnInit(_remoteEnemyData.SpeedBase, this.CachedTransform);
_movementComponent.OnInit(_remoteEnemyData.SpeedBase, this.CachedTransform, null, true);
_movementComponent.SetMove(true);
_attackRangeSquared = _attackRange * _attackRange;
this.CachedTransform.position = enemyData.Position;
@@ -52,6 +52,13 @@ namespace Entity
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
if (_target == null)
{
_movementComponent.SetMove(false);
_movementComponent.OnUpdate(elapseSeconds, realElapseSeconds);
return;
}
float distanceSquared = (this.CachedTransform.position - _target.position).sqrMagnitude;
if (distanceSquared < _attackRangeSquared)
{
@@ -77,6 +84,11 @@ namespace Entity
private Vector3 GetTargetDirection()
{
if (_target == null)
{
return Vector3.zero;
}
return new Vector3(
_target.position.x - this.CachedTransform.position.x,
0f,
@@ -84,4 +96,4 @@ namespace Entity
).normalized;
}
}
}
}
@@ -6,6 +6,7 @@ using Definition.DataStruct;
using Entity.EntityData;
using GameFramework.Event;
using UnityGameFramework.Runtime;
using Entity.Weapon;
namespace Entity
{
@@ -45,6 +46,7 @@ namespace Entity
/// 玩家是否启用。
/// </summary>
private bool _enable;
private bool _weaponEnabled = true;
public int Coin
@@ -89,6 +91,22 @@ namespace Entity
return _backpackComponent.AttachProp(prop);
}
public bool RemoveWeapon(WeaponBase weapon)
{
if (weapon == null || _backpackComponent == null)
{
return false;
}
if (!_backpackComponent.DetachWeapon(weapon))
{
return false;
}
GameEntry.Entity.HideEntity(weapon);
return true;
}
public bool Enable
{
get => _enable;
@@ -122,8 +140,7 @@ namespace Entity
// BackpackComponent
Coin = role.Coin;
_backpackComponent.OnInit(this, role.WeaponCapacity);
GameEntry.Entity.ShowWeapon(new WeaponKnifeData(GameEntry.Entity.GenerateSerialId(), 201,
this.Id, _playerData.Camp));
GameEntry.Entity.ShowWeapon(new WeaponKnifeData(GameEntry.Entity.GenerateSerialId(), Id, _playerData.Camp));
// StatComponent
foreach (var modifier in role.InitialProperties)
@@ -8,7 +8,7 @@
using Components;
using Definition.DataStruct;
using Entity.EntityData;
using Game.Utility;
using CustomUtility;
using UnityEngine;
namespace Entity
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: abdf25a9782ebf844a65b2348f8f566d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,54 @@
using UnityEngine;
namespace Entity.Weapon
{
public sealed class HandgunHitMarkerAttackEffect : IWeaponAttackEffect
{
private readonly float _size;
private readonly float _yOffset;
private readonly float _duration;
private readonly Color _color;
public HandgunHitMarkerAttackEffect(float size, float yOffset, float duration, Color color)
{
_size = size;
_yOffset = yOffset;
_duration = duration;
_color = color;
}
public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
{
if (target == null) return;
GameObject marker = GameObject.CreatePrimitive(PrimitiveType.Sphere);
marker.name = "HandgunHitMarker";
Collider collider = marker.GetComponent<Collider>();
if (collider != null)
{
Object.Destroy(collider);
}
marker.transform.SetParent(target.CachedTransform, false);
marker.transform.localPosition = new Vector3(0f, _yOffset, 0f);
marker.transform.localScale = Vector3.one * Mathf.Max(0.01f, _size);
Renderer renderer = marker.GetComponent<Renderer>();
if (renderer != null)
{
Shader shader = Shader.Find("Sprites/Default");
if (shader == null)
{
shader = Shader.Find("Unlit/Color");
}
Material material = new Material(shader);
material.color = _color;
renderer.material = material;
}
Object.Destroy(marker, Mathf.Max(0.01f, _duration));
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d944f33383dc4c1ebe850a12d4211afa
timeCreated: 1771481130
@@ -0,0 +1,9 @@
using UnityEngine;
namespace Entity.Weapon
{
public interface IWeaponAttackEffect
{
void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius);
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 231a935a484c4351b707b3e359074a47
timeCreated: 1771481068
@@ -0,0 +1,63 @@
using UnityEngine;
namespace Entity.Weapon
{
public sealed class KnifeRangeAttackEffect : IWeaponAttackEffect
{
private readonly float _duration = 0.2f;
private readonly float _yOffset = 0.05f;
private readonly float _lineWidth = 0.05f;
private readonly int _segments = 48;
private readonly Color _color = new(0.1f, 1f, 0.1f, 0.9f);
public KnifeRangeAttackEffect()
{
}
public KnifeRangeAttackEffect(float duration, float yOffset, float lineWidth, int segments, Color color)
{
_duration = duration;
_yOffset = yOffset;
_lineWidth = lineWidth;
_segments = Mathf.Max(8, segments);
_color = color;
}
public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
{
float safeRadius = Mathf.Max(0.1f, radius);
GameObject indicator = new GameObject("KnifeRangeIndicator");
indicator.transform.position = new Vector3(position.x, position.y + _yOffset, position.z);
LineRenderer line = indicator.AddComponent<LineRenderer>();
line.loop = true;
line.useWorldSpace = true;
line.positionCount = _segments;
line.startWidth = _lineWidth;
line.endWidth = _lineWidth;
line.startColor = _color;
line.endColor = _color;
Shader shader = Shader.Find("Sprites/Default");
if (shader == null)
{
shader = Shader.Find("Unlit/Color");
}
Material material = new Material(shader);
material.color = _color;
line.material = material;
float step = Mathf.PI * 2f / _segments;
for (int i = 0; i < _segments; i++)
{
float angle = i * step;
Vector3 offset = new Vector3(Mathf.Cos(angle) * safeRadius, 0f, Mathf.Sin(angle) * safeRadius);
line.SetPosition(i, indicator.transform.position + offset);
}
Object.Destroy(indicator, Mathf.Max(0.01f, _duration));
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8f48328040644546bf0a866cc337d3b2
timeCreated: 1771481100
@@ -0,0 +1,86 @@
using UnityEngine;
namespace Entity.Weapon
{
public sealed class SlashSectorAttackEffect : IWeaponAttackEffect
{
private readonly float _duration = 0.2f;
private readonly float _yOffset = 0.06f;
private readonly float _lineWidth = 0.05f;
private readonly int _segments = 24;
private readonly Color _color = new(1f, 0.45f, 0.2f, 0.9f);
public SlashSectorAttackEffect()
{
}
public SlashSectorAttackEffect(float duration, float yOffset, float lineWidth, int segments, Color color)
{
_duration = duration;
_yOffset = yOffset;
_lineWidth = lineWidth;
_segments = Mathf.Max(6, segments);
_color = color;
}
public void Play(WeaponBase weapon, Vector3 position, EntityBase target, float radius)
{
if (weapon is not WeaponSlash slash) return;
float safeRadius = Mathf.Max(0.1f, radius);
float sectorAngle = Mathf.Clamp(slash.SectorAngle, 1f, 360f);
GameObject indicator = new GameObject("SlashSectorIndicator");
Vector3 center = new Vector3(position.x, position.y + _yOffset, position.z);
indicator.transform.position = center;
LineRenderer line = indicator.AddComponent<LineRenderer>();
line.loop = false;
line.useWorldSpace = true;
line.startWidth = _lineWidth;
line.endWidth = _lineWidth;
line.startColor = _color;
line.endColor = _color;
Shader shader = Shader.Find("Sprites/Default");
if (shader == null)
{
shader = Shader.Find("Unlit/Color");
}
Material material = new Material(shader);
material.color = _color;
line.material = material;
float half = sectorAngle * 0.5f;
int arcPoints = _segments + 1;
int positionCount = arcPoints + 2;
line.positionCount = positionCount;
Vector3 forward = weapon.CachedTransform.forward;
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
{
forward = Vector3.forward;
}
forward.Normalize();
Quaternion baseRotation = Quaternion.LookRotation(forward, Vector3.up);
line.SetPosition(0, center);
for (int i = 0; i < arcPoints; i++)
{
float t = arcPoints <= 1 ? 0f : i / (float)(arcPoints - 1);
float angle = Mathf.Lerp(-half, half, t);
Vector3 dir = Quaternion.Euler(0f, angle, 0f) * Vector3.forward;
Vector3 worldDir = baseRotation * dir;
line.SetPosition(i + 1, center + worldDir * safeRadius);
}
line.SetPosition(positionCount - 1, center);
UnityEngine.Object.Destroy(indicator, Mathf.Max(0.01f, _duration));
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 329af2c156b345f28a7465ef29c943da
timeCreated: 1771485834
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 371bc24a18a3447686043843fdd6fbe5
timeCreated: 1771427642
@@ -0,0 +1,49 @@
using System.Collections.Generic;
using Components;
using CustomUtility;
namespace Entity.Weapon
{
public sealed class HighestHealthTargetSelector : ITargetSelector
{
public EntityBase SelectTarget(WeaponBase weapon, IEnumerable<EntityBase> candidates, float maxSqrRange)
{
if (weapon == null || candidates == null)
{
return null;
}
EntityBase target = null;
float maxHp = float.MinValue;
float maxSqr = maxSqrRange > 0f ? maxSqrRange : float.MaxValue;
foreach (var candidate in candidates)
{
if (candidate == null || !candidate.Available) continue;
if (maxSqrRange > 0f && AIUtility.GetSqrMagnitudeXZ(weapon, candidate) >= maxSqr) continue;
if (!TryGetCurrentHealth(candidate, out var hp)) continue;
if (hp <= maxHp) continue;
maxHp = hp;
target = candidate;
}
return target;
}
private static bool TryGetCurrentHealth(EntityBase target, out float health)
{
health = 0f;
if (target == null) return false;
var healthComponent = target.GetComponent<HealthComponent>();
if (healthComponent == null) return false;
health = healthComponent.CurrentHealth;
return true;
}
}
}
@@ -1,8 +1,7 @@
fileFormatVersion: 2
guid: 822be177ff3fd674fa34da0755f1a427
timeCreated: 1528026153
licenseType: Pro
guid: 54932952c9fa3fd4dbe6d21f2a484b4d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace Entity.Weapon
{
public interface ITargetSelector
{
EntityBase SelectTarget(WeaponBase weapon, IEnumerable<EntityBase> candidates, float maxSqrRange);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1ed801e4816c03b4597756dc36d6aba8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using Components;
using Definition.Enum;
using CustomUtility;
namespace Entity.Weapon
{
public sealed class LowestHealthTargetSelector : ITargetSelector
{
public EntityBase SelectTarget(WeaponBase weapon, IEnumerable<EntityBase> candidates, float maxSqrRange)
{
if (weapon == null || candidates == null)
{
return null;
}
EntityBase target = null;
float minHp = float.MaxValue;
float maxSqr = maxSqrRange > 0f ? maxSqrRange : float.MaxValue;
foreach (var candidate in candidates)
{
if (candidate == null || !candidate.Available) continue;
if (maxSqrRange > 0f && AIUtility.GetSqrMagnitudeXZ(weapon, candidate) >= maxSqr) continue;
if (!TryGetHealth(candidate, out var hp)) continue;
if (hp >= minHp) continue;
minHp = hp;
target = candidate;
}
return target;
}
private static bool TryGetHealth(EntityBase target, out float health)
{
health = 0f;
if (target == null) return false;
var statComponent = target.GetComponent<StatComponent>();
if (statComponent == null) return false;
var hpStat = statComponent.GetStat(StatType.MaxHealth);
if (hpStat == null) return false;
health = hpStat.Value;
return true;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 081d67a491f5792479306d88091168e3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using System.Collections.Generic;
using CustomUtility;
namespace Entity.Weapon
{
public sealed class NearestTargetSelector : ITargetSelector
{
public EntityBase SelectTarget(WeaponBase weapon, IEnumerable<EntityBase> candidates, float maxSqrRange)
{
if (weapon == null || candidates == null)
{
return null;
}
EntityBase target = null;
float minSqrMagnitude = maxSqrRange > 0f ? maxSqrRange : float.MaxValue;
foreach (var candidate in candidates)
{
if (candidate == null || !candidate.Available) continue;
float sqrMagnitude = AIUtility.GetSqrMagnitudeXZ(weapon, candidate);
if (sqrMagnitude >= minSqrMagnitude) continue;
minSqrMagnitude = sqrMagnitude;
target = candidate;
}
return target;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a70602535b318e74396a6c2ea282834d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
namespace Entity.Weapon
{
public enum TargetSelectorType
{
Nearest = 0,
HighestHealth = 1,
LowestHealth = 2
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4eb1e0b99139fdd4280c8616804a8f77
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,18 +1,19 @@
using System;
using System.Collections.Generic;
using Components;
using Definition.DataStruct;
using Definition.Enum;
using Entity.EntityData;
using GameFramework;
using CustomUtility;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity
namespace Entity.Weapon
{
public enum WeaponStateType
{
Disabled,
Check,
Attack,
Transition,
Idle,
Check_OutRange,
Check_InRange,
@@ -29,37 +30,94 @@ namespace Entity
[SerializeField] protected bool _isEnabled = false;
public WeaponData WeaponData;
public WeaponData WeaponData { get; protected set; }
public bool IsAttacking => _isAttacking;
protected ITargetSelector TargetSelector { get; set; }
protected Dictionary<WeaponStateType, WeaponStateBase> _states;
protected WeaponStateBase _currentState;
#region FSM
protected EntityBase _target;
protected float _currAttackTimer;
protected float _sqrRange;
protected StatProperty AttackStat { get; private set; } = new();
private StatComponent _attackStatComponent;
private System.Action<StatModifier, bool> _attackStatCallback;
private static readonly List<EntityBase> s_EmptyCandidates = new();
#region Lifecycle
protected override void OnInit(object userData)
{
base.OnInit(userData);
if (TargetSelector == null)
{
TargetSelector = CreateSelector(DefaultTargetSelectorType);
}
}
//protected abstract void OnShow(object userData);
// {
// base.OnShow(userData);
//
// _weaponData = userData as WeaponData;
// if (_weaponData == null)
// {
// Log.Error("Weapon data is invalid.");
// return;
// }
// }
protected sealed override void OnShow(object userData)
{
base.OnShow(userData);
protected override void OnAttachTo(EntityLogic parentEntity, Transform parentTransform, object userData)
if (!OnWeaponShow(userData)) return;
EnsureStatesBuilt();
TransitionTo(InitialState);
}
protected sealed override void OnHide(bool isShutdown, object userData)
{
ReleaseAttackStatSubscription();
OnWeaponHide(userData);
base.OnHide(isShutdown, userData);
}
protected sealed override void OnAttachTo(EntityLogic parentEntity, Transform parentTransform, object userData)
{
base.OnAttachTo(parentEntity, parentTransform, userData);
Name = Utility.Text.Format("Weapon of {0}", parentEntity.Name);
CachedTransform.localPosition = Vector3.zero;
OnWeaponAttach(parentEntity, parentTransform, userData);
}
protected sealed override void OnDetachFrom(EntityLogic parentEntity, object userData)
{
OnWeaponDetach(parentEntity, userData);
base.OnDetachFrom(parentEntity, userData);
}
protected virtual bool OnWeaponShow(object userData) => true;
protected virtual void OnWeaponHide(object userData)
{
}
protected virtual void OnWeaponAttach(EntityLogic parentEntity, Transform parentTransform, object userData)
{
}
protected virtual void OnWeaponDetach(EntityLogic parentEntity, object userData)
{
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
if (!_isEnabled) return;
_currentState?.OnUpdate(elapseSeconds, realElapseSeconds);
}
#endregion
@@ -67,12 +125,147 @@ namespace Entity
public abstract ImpactData GetImpactData();
protected abstract void Attack();
protected abstract void Check();
protected abstract void TransitionTo(WeaponStateType newState);
protected abstract void BuildStates();
protected virtual WeaponStateType InitialState => WeaponStateType.Idle;
protected virtual TargetSelectorType DefaultTargetSelectorType => TargetSelectorType.Nearest;
protected void RegisterState(WeaponStateBase state)
{
if (state == null)
{
Log.Error("Weapon state is invalid.");
return;
}
state.OnInit(this);
_states[state.State] = state;
}
protected void EnsureStatesBuilt()
{
if (_states != null) return;
_states = new Dictionary<WeaponStateType, WeaponStateBase>();
BuildStates();
}
protected virtual void TransitionTo(WeaponStateType newState)
{
if (_states == null || !_states.TryGetValue(newState, out var nextState))
{
Log.Error("Weapon state not found: {0}", newState);
return;
}
_currentState?.OnLeave();
_currentState = nextState;
_currentState.OnEnter();
}
public void SetEnabled(bool value)
{
this._isEnabled = value;
TransitionTo(WeaponStateType.Idle);
_isEnabled = value;
OnEnabledChanged(value);
if (_states == null || _states.Count == 0) return;
if (value)
{
TransitionTo(InitialState);
return;
}
if (_states.ContainsKey(WeaponStateType.Disabled))
{
TransitionTo(WeaponStateType.Disabled);
}
}
protected virtual void OnEnabledChanged(bool enabled)
{
}
protected T RequireWeaponData<T>(object userData) where T : WeaponData
{
var data = userData as T;
if (data == null)
{
Log.Error("{0} data is invalid.", typeof(T).Name);
}
return data;
}
protected virtual IEnumerable<EntityBase> GetCandidates()
{
var enemyManager = GameEntry.EnemyManager;
return enemyManager != null ? enemyManager.Enemies : s_EmptyCandidates;
}
protected EntityBase SelectTarget(float maxSqrRange)
{
return TargetSelector != null ? TargetSelector.SelectTarget(this, GetCandidates(), maxSqrRange) : null;
}
protected float GetSqrDistance(EntityBase target)
{
return target != null ? AIUtility.GetSqrMagnitudeXZ(this, target) : float.MaxValue;
}
protected bool IsInRange(EntityBase target, float sqrRange)
{
if (target == null) return false;
return AIUtility.GetSqrMagnitudeXZ(this, target) < sqrRange;
}
protected void SetTargetSelector(TargetSelectorType selectorType)
{
TargetSelector = CreateSelector(selectorType);
}
protected static ITargetSelector CreateSelector(TargetSelectorType selectorType)
{
switch (selectorType)
{
case TargetSelectorType.HighestHealth:
return new HighestHealthTargetSelector();
case TargetSelectorType.LowestHealth:
return new LowestHealthTargetSelector();
case TargetSelectorType.Nearest:
default:
return new NearestTargetSelector();
}
}
protected void BindAttackStatFromOwner(EntityLogic parentEntity)
{
ReleaseAttackStatSubscription();
AttackStat = new StatProperty();
if (parentEntity is not Player player) return;
var statComponent = player.GetComponent<StatComponent>();
if (statComponent == null) return;
AttackStat = statComponent.GetStat(StatType.Attack);
_attackStatComponent = statComponent;
_attackStatCallback = (modifier, isApply) =>
statComponent.UpdateStat(AttackStat, modifier, isApply);
statComponent.Subscribe(StatType.Attack, _attackStatCallback);
}
protected void ReleaseAttackStatSubscription()
{
if (_attackStatComponent != null && _attackStatCallback != null)
{
_attackStatComponent.Unsubscribe(StatType.Attack, _attackStatCallback);
}
_attackStatComponent = null;
_attackStatCallback = null;
AttackStat = new StatProperty();
}
}
@@ -85,4 +278,12 @@ namespace Entity
public abstract void OnLeave();
public override string ToString() => State.ToString();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0b289e88dc5243aeb564df941485c5cf
timeCreated: 1771579168
@@ -0,0 +1,25 @@
namespace Entity.Weapon
{
public partial class WeaponHandgun
{
private class AttackState : WeaponStateBase
{
private WeaponHandgun _weapon;
public override WeaponStateType State => WeaponStateType.Attack;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponHandgun;
public override void OnEnter()
{
_weapon._currAttackTimer = 0f;
_weapon.Attack();
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
public override void OnLeave() { }
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4e72df067466464fa3be92bfd049dd0d
timeCreated: 1771579220
@@ -0,0 +1,46 @@
namespace Entity.Weapon
{
public partial class WeaponHandgun
{
private class CheckInRangeState : WeaponStateBase
{
private WeaponHandgun _weapon;
public override WeaponStateType State => WeaponStateType.Check_InRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponHandgun;
public override void OnEnter()
{
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (!_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
return;
}
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnLeave() { }
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bfd76c0fb7284ceaa28f78c702f595da
timeCreated: 1771579231
@@ -0,0 +1,33 @@
namespace Entity.Weapon
{
public partial class WeaponHandgun
{
private class CheckOutRangeState : WeaponStateBase
{
private WeaponHandgun _weapon;
public override WeaponStateType State => WeaponStateType.Check_OutRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponHandgun;
public override void OnEnter() { }
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave() { }
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3a9f936d3c8643f7bf632734548f800c
timeCreated: 1771579241
@@ -0,0 +1,27 @@
namespace Entity.Weapon
{
public partial class WeaponHandgun
{
private class IdleState : WeaponStateBase
{
private WeaponHandgun _weapon;
public override WeaponStateType State => WeaponStateType.Idle;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponHandgun;
public override void OnEnter() { }
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToOrigin(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target != null && _weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
}
}
public override void OnLeave() { }
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9c5341944d1a467b8553d31bc3d81d5b
timeCreated: 1771579252
@@ -0,0 +1,143 @@
using Definition.DataStruct;
using Definition.Enum;
using Entity.EntityData;
using CustomUtility;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity.Weapon
{
public partial class WeaponHandgun : WeaponBase
{
#region Property
private WeaponHandgunData _weaponData;
private Quaternion _cachedRotation;
[SerializeField] private float _rotateSpeed = 6f;
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private Vector3 _fireOriginOffset = Vector3.zero;
[SerializeField] private float _hitMarkerSize = 0.2f;
[SerializeField] private float _hitMarkerYOffset = 1.2f;
[SerializeField] private float _hitMarkerDuration = 0.15f;
[SerializeField] private Color _hitMarkerColor = new(1f, 0f, 0f, 0.95f);
private IWeaponAttackEffect _attackEffect;
#endregion
public override ImpactData GetImpactData()
{
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, AttackStat);
}
protected override void BuildStates()
{
RegisterState(new IdleState());
RegisterState(new CheckOutRangeState());
RegisterState(new CheckInRangeState());
RegisterState(new AttackState());
}
protected override void Attack()
{
FaceTargetImmediately();
Vector3 fireOrigin = CachedTransform.TransformPoint(_fireOriginOffset);
Vector3 fireDirection = CachedTransform.forward;
float maxDistance = Mathf.Max(0.1f, _weaponData.AttackRange);
if (Physics.Raycast(fireOrigin, fireDirection, out RaycastHit hit, maxDistance, _hitMask,
QueryTriggerInteraction.Collide))
{
TargetableObject targetable = hit.collider.GetComponentInParent<TargetableObject>();
if (targetable != null && targetable.Available && !targetable.IsDead)
{
_attackEffect?.Play(this, hit.point, targetable, 0f);
_isAttacking = true;
AIUtility.PerformCollision(targetable, this);
_isAttacking = false;
}
}
}
protected override void Check()
{
_target = SelectTarget(_sqrRange);
}
private void RotateToTarget(float elapseSeconds)
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = (_target.CachedTransform.position - CachedTransform.position).normalized;
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget, Vector3.up);
CachedTransform.rotation = Quaternion.Slerp(CachedTransform.rotation, targetRotation,
_rotateSpeed * elapseSeconds);
}
private void RotateToOrigin(float elapseSeconds)
{
CachedTransform.rotation = Quaternion.Slerp(CachedTransform.rotation, _cachedRotation,
_rotateSpeed * elapseSeconds);
}
private void FaceTargetImmediately()
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
}
#region Lifecycle
protected override bool OnWeaponShow(object userData)
{
_weaponData = RequireWeaponData<WeaponHandgunData>(userData);
if (_weaponData == null) return false;
WeaponData = _weaponData;
_currAttackTimer = 0f;
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
_attackEffect = new HandgunHitMarkerAttackEffect(_hitMarkerSize, _hitMarkerYOffset, _hitMarkerDuration,
_hitMarkerColor);
if (_weaponData.OwnerCamp == CampType.Player)
{
gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
_hitMask = LayerMask.GetMask("Enemy");
}
else if (_weaponData.OwnerCamp == CampType.Enemy)
{
gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
_hitMask = LayerMask.GetMask("Player");
}
return true;
}
protected override void OnWeaponHide(object userData)
{
_attackEffect = null;
}
protected override void OnWeaponAttach(EntityLogic parentEntity, Transform parentTransform, object userData)
{
BindAttackStatFromOwner(parentEntity);
}
protected override void OnWeaponDetach(EntityLogic parentEntity, object userData)
{
ReleaseAttackStatSubscription();
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 44ae1bdd1d43e274980743df21658730
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,333 +0,0 @@
using System;
using System.Collections.Generic;
using Components;
using CustomComponent;
using Definition.DataStruct;
using Definition.Enum;
using DG.Tweening;
using Entity.EntityData;
using Game.Utility;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity
{
public class WeaponKnife : WeaponBase
{
private WeaponKnifeData _weaponData;
private float _currAttackTimer = 0f;
private EnemyManagerComponent _enemy;
private EntityBase _target;
private float _sqrRange;
private Collider _collider;
private Rigidbody _rigidbody;
private Quaternion _cachedRotation;
[SerializeField] private float _rotateSpeed = 2f;
private Dictionary<WeaponStateType, WeaponStateBase> _weaponStates;
private StatProperty _attackStat = new();
private Action<StatModifier, bool> _attackStatCallback;
[SerializeField] private float attackDuration = 0.15f; // 戳刺持续时间
[SerializeField] private float returnDuration = 0.2f; // 回归持续时间
public override ImpactData GetImpactData()
{
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, _attackStat);
}
private void InitStates()
{
_weaponStates = new Dictionary<WeaponStateType, WeaponStateBase>
{
{ WeaponStateType.Idle, new IdleState() },
{ WeaponStateType.Check_InRange, new CheckInRangeState() },
{ WeaponStateType.Check_OutRange, new CheckOutRangeState() },
{ WeaponStateType.Attack, new AttackState() }
};
foreach (var state in _weaponStates.Values)
{
state.OnInit(this);
}
}
#region FSM Methods
protected override void Attack()
{
Vector3 targetPos = CachedTransform.position + CachedTransform.forward * _weaponData.AttackRange;
_isAttacking = true;
_collider.enabled = true;
_rigidbody.detectCollisions = true;
Transform parentTransform = CachedTransform.parent;
CachedTransform.SetParent(null);
var sequence = DOTween.Sequence();
sequence.Append(CachedTransform.DOMove(targetPos, attackDuration).SetEase(Ease.OutQuad));
sequence.AppendCallback(() =>
{
_collider.enabled = false;
_rigidbody.detectCollisions = false;
CachedTransform.SetParent(parentTransform);
});
sequence.Append(CachedTransform.DOLocalMove(Vector3.zero, returnDuration / 2f).SetEase(Ease.InQuad));
sequence.AppendCallback(() => { _isAttacking = false; });
}
protected override void Check()
{
_target = null;
float minSqrMagnitude = float.MaxValue;
var enemies = _enemy.Enemies;
foreach (var enemy in enemies)
{
if (enemy == null || !enemy.Available) continue;
float sqrMagnitude = AIUtility.GetSqrMagnitude(this, enemy);
if (minSqrMagnitude < sqrMagnitude) continue;
_target = enemy;
minSqrMagnitude = sqrMagnitude;
}
}
private void RotateToTarget(float elapseSeconds)
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = (_target.CachedTransform.position - transform.position).normalized;
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget, Vector3.up);
var quaternion = Quaternion.Slerp(CachedTransform.rotation, targetRotation, _rotateSpeed * elapseSeconds);
CachedTransform.rotation = quaternion;
}
private void RotateToOrigin(float elapseSeconds)
{
var quaternion = Quaternion.Slerp(CachedTransform.rotation, _cachedRotation, _rotateSpeed * elapseSeconds);
CachedTransform.rotation = quaternion;
}
protected override void TransitionTo(WeaponStateType state)
{
_currentState?.OnLeave();
_currentState = _weaponStates[state];
_currentState.OnEnter();
}
#endregion
#region FSM
protected override void OnShow(object userData)
{
base.OnShow(userData);
_weaponData = userData as WeaponKnifeData;
if (_weaponData == null)
{
Log.Error("WeaponKnife data is invalid.");
return;
}
WeaponData = _weaponData;
_collider = GetComponent<Collider>();
_rigidbody = GetComponent<Rigidbody>();
_currAttackTimer = 0;
_enemy = GameEntry.EnemyManager;
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = transform.rotation;
_collider.enabled = false;
_rigidbody.detectCollisions = false;
if (_weaponData.OwnerCamp == CampType.Player)
this.gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
if (_weaponData.OwnerCamp == CampType.Enemy)
this.gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
InitStates();
TransitionTo(WeaponStateType.Idle);
}
protected override void OnAttachTo(EntityLogic parentEntity, Transform parentTransform, object userData)
{
base.OnAttachTo(parentEntity, parentTransform, userData);
if (parentEntity is Player player)
{
var statComponent = player.GetComponent<StatComponent>();
if (statComponent != null)
{
this._attackStat = statComponent.GetStat(StatType.Attack);
this._attackStatCallback = (modifier, b) => statComponent.UpdateStat(_attackStat, modifier, b);
statComponent.Subscribe(StatType.Attack, this._attackStatCallback);
}
}
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
if (!_isEnabled) return;
Log.Debug($"current state: {_currentState}");
_currentState?.OnUpdate(elapseSeconds, realElapseSeconds);
}
protected override void OnDetachFrom(EntityLogic parentEntity, object userData)
{
base.OnDetachFrom(parentEntity, userData);
if (parentEntity is Player player)
{
var statComponent = player.GetComponent<StatComponent>();
if (statComponent != null)
{
statComponent.Unsubscribe(StatType.Attack, this._attackStatCallback);
}
}
this._attackStat = null;
this._attackStatCallback = null;
}
#endregion
private class IdleState : WeaponStateBase
{
private WeaponKnife _weapon;
public override WeaponStateType State => WeaponStateType.Idle;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponKnife;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToOrigin(elapseSeconds);
if (_weapon._target != null && _weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
}
}
public override void OnLeave()
{
}
}
private class CheckOutRangeState : WeaponStateBase
{
private WeaponKnife _weapon;
public override WeaponStateType State => WeaponStateType.Check_OutRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponKnife;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (AIUtility.GetSqrMagnitude(_weapon._target, _weapon) < _weapon._sqrRange)
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
return;
}
}
public override void OnLeave()
{
}
}
private class CheckInRangeState : WeaponStateBase
{
private WeaponKnife _weapon;
public override WeaponStateType State => WeaponStateType.Check_InRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponKnife;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (AIUtility.GetSqrMagnitude(_weapon._target, _weapon) >= _weapon._sqrRange)
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
return;
}
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
return;
}
}
public override void OnLeave()
{
_weapon._currAttackTimer = 0;
}
}
private class AttackState : WeaponStateBase
{
private WeaponKnife _weapon;
public override WeaponStateType State => WeaponStateType.Attack;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponKnife;
public override void OnEnter()
{
_weapon.Attack();
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_weapon._isAttacking)
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f89893f21b3046b7a93a368cd688451e
timeCreated: 1771578597
@@ -0,0 +1,31 @@
namespace Entity.Weapon
{
public partial class WeaponKnife
{
private class AttackState : WeaponStateBase
{
private WeaponKnife _weapon;
public override WeaponStateType State => WeaponStateType.Attack;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponKnife;
public override void OnEnter()
{
_weapon._currAttackTimer = 0f;
_weapon.Attack();
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_weapon._isAttacking)
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 417516ffe5574e0c998105fcaa073de7
timeCreated: 1771578955
@@ -0,0 +1,49 @@
namespace Entity.Weapon
{
public partial class WeaponKnife
{
private class CheckInRangeState : WeaponStateBase
{
private WeaponKnife _weapon;
public override WeaponStateType State => WeaponStateType.Check_InRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponKnife;
public override void OnEnter()
{
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (!_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
return;
}
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 59644093364644768c26261b24bdb820
timeCreated: 1771578916
@@ -0,0 +1,39 @@
namespace Entity.Weapon
{
public partial class WeaponKnife
{
private class CheckOutRangeState : WeaponStateBase
{
private WeaponKnife _weapon;
public override WeaponStateType State => WeaponStateType.Check_OutRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponKnife;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d9e1a63869694e72a360afe29cc94cfb
timeCreated: 1771578994
@@ -0,0 +1,33 @@
namespace Entity.Weapon
{
public partial class WeaponKnife
{
public class IdleState : WeaponStateBase
{
private WeaponKnife _weapon;
public override WeaponStateType State => WeaponStateType.Idle;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponKnife;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToOrigin(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target != null && _weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b4d66ec44b064640aa95c6aa71ff3846
timeCreated: 1771578662
@@ -0,0 +1,227 @@
using System.Collections.Generic;
using CustomUtility;
using Definition.DataStruct;
using Definition.Enum;
using DG.Tweening;
using Entity.EntityData;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity.Weapon
{
public partial class WeaponKnife : WeaponBase
{
private const string HitRadiusParamKey = "HitRadius";
private WeaponKnifeData _weaponData;
private Quaternion _cachedRotation;
[SerializeField] private float _rotateSpeed = 4f;
private Sequence _attackSequence;
private Transform _attackParent;
private Vector3 _attackCenter;
[SerializeField] private float attackDuration = 0.15f;
[SerializeField] private float returnDuration = 0.2f;
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private int _maxHitColliders = 32;
private IWeaponAttackEffect _attackEffect;
private Collider[] _hitResults;
private readonly HashSet<int> _hitEntityIds = new();
private float _hitRadius;
private float _hitRadiusSqr;
public override ImpactData GetImpactData()
{
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, AttackStat);
}
protected override void BuildStates()
{
RegisterState(new IdleState());
RegisterState(new CheckInRangeState());
RegisterState(new CheckOutRangeState());
RegisterState(new AttackState());
}
protected override void Attack()
{
StopAttackTween(false);
FaceTargetImmediately();
_isAttacking = true;
Transform parentTransform = CachedTransform.parent;
_attackParent = parentTransform;
_attackCenter = _target != null ? _target.CachedTransform.position : CachedTransform.position;
CachedTransform.SetParent(null);
Vector3 targetPos = CachedTransform.position + CachedTransform.forward * _weaponData.AttackRange;
_attackSequence = DOTween.Sequence();
_attackSequence.Append(CachedTransform.DOMove(targetPos, attackDuration).SetEase(Ease.OutQuad));
_attackSequence.AppendCallback(() =>
{
_attackEffect?.Play(this, _attackCenter, null, _hitRadius);
ApplyGroundAreaDamage();
if (_attackParent != null)
{
CachedTransform.SetParent(_attackParent);
}
});
_attackSequence.Append(
CachedTransform.DOLocalMove(Vector3.zero, returnDuration * 0.5f).SetEase(Ease.InQuad));
_attackSequence.AppendCallback(() =>
{
_isAttacking = false;
_attackSequence = null;
_attackParent = null;
});
}
protected override void Check()
{
_target = SelectTarget(_sqrRange);
}
private void RotateToTarget(float elapseSeconds)
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
CachedTransform.rotation =
Quaternion.Slerp(CachedTransform.rotation, targetRotation, _rotateSpeed * elapseSeconds);
}
public void RotateToOrigin(float elapseSeconds)
{
CachedTransform.rotation =
Quaternion.Slerp(CachedTransform.rotation, _cachedRotation, _rotateSpeed * elapseSeconds);
}
private void FaceTargetImmediately()
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
}
private void ApplyGroundAreaDamage()
{
if (_hitRadius <= 0f || _hitResults == null || _hitResults.Length == 0) return;
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _hitRadius, _hitResults, _hitMask,
QueryTriggerInteraction.Collide);
_hitEntityIds.Clear();
for (int i = 0; i < hitCount; i++)
{
Collider collider = _hitResults[i];
if (collider == null) continue;
TargetableObject targetable = collider.GetComponentInParent<TargetableObject>();
if (targetable == null || !targetable.Available || targetable.IsDead) continue;
if (!_hitEntityIds.Add(targetable.Id)) continue;
Vector3 delta = targetable.CachedTransform.position - _attackCenter;
delta.y = 0f;
if (delta.sqrMagnitude > _hitRadiusSqr) continue;
AIUtility.PerformCollision(targetable, this);
}
}
protected override bool OnWeaponShow(object userData)
{
_weaponData = RequireWeaponData<WeaponKnifeData>(userData);
if (_weaponData == null) return false;
WeaponData = _weaponData;
_currAttackTimer = 0f;
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
string hitRadiusRaw = _weaponData.GetParamsString(HitRadiusParamKey);
if (!float.TryParse(hitRadiusRaw, out _hitRadius))
{
_hitRadius = _weaponData.AttackRange;
}
_hitRadius = Mathf.Max(0.1f, _hitRadius);
_hitRadiusSqr = _hitRadius * _hitRadius;
_attackEffect = new KnifeRangeAttackEffect();
int colliderCapacity = Mathf.Max(1, _maxHitColliders);
if (_hitResults == null || _hitResults.Length != colliderCapacity)
{
_hitResults = new Collider[colliderCapacity];
}
if (_weaponData.OwnerCamp == CampType.Player)
{
gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
_hitMask = LayerMask.GetMask("Enemy");
}
else if (_weaponData.OwnerCamp == CampType.Enemy)
{
gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
_hitMask = LayerMask.GetMask("Player");
}
return true;
}
protected override void OnWeaponHide(object userData)
{
StopAttackTween(true);
_attackEffect = null;
}
protected override void OnWeaponAttach(EntityLogic parentEntity, Transform parentTransform, object userData)
{
BindAttackStatFromOwner(parentEntity);
}
protected override void OnWeaponDetach(EntityLogic parentEntity, object userData)
{
StopAttackTween(true);
ReleaseAttackStatSubscription();
}
protected override void OnEnabledChanged(bool enabled)
{
if (!enabled)
{
StopAttackTween(true);
}
}
private void StopAttackTween(bool resetTransform)
{
if (_attackSequence != null)
{
_attackSequence.Kill();
_attackSequence = null;
}
_isAttacking = false;
if (resetTransform && _attackParent != null)
{
CachedTransform.SetParent(_attackParent);
CachedTransform.localPosition = Vector3.zero;
}
_attackParent = null;
_hitEntityIds.Clear();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9289c110a12b456aabb374427caa9e1a
timeCreated: 1771579177
@@ -0,0 +1,30 @@
namespace Entity.Weapon
{
public partial class WeaponSlash
{
private class AttackState : WeaponStateBase
{
private WeaponSlash _weapon;
public override WeaponStateType State => WeaponStateType.Attack;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponSlash;
public override void OnEnter()
{
_weapon._currAttackTimer = 0f;
_weapon.Attack();
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (!_weapon._isAttacking)
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 96b136ec326941a0821f66ddf750de29
timeCreated: 1771579419
@@ -0,0 +1,48 @@
namespace Entity.Weapon
{
public partial class WeaponSlash
{
private class CheckInRangeState : WeaponStateBase
{
private WeaponSlash _weapon;
public override WeaponStateType State => WeaponStateType.Check_InRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponSlash;
public override void OnEnter()
{
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (!_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
return;
}
if (_weapon._currAttackTimer >= _weapon._weaponData.Cooldown)
{
_weapon.TransitionTo(WeaponStateType.Attack);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 57a936178b5a4ff2938df369b77631e6
timeCreated: 1771579430
@@ -0,0 +1,38 @@
namespace Entity.Weapon
{
public partial class WeaponSlash
{
private class CheckOutRangeState : WeaponStateBase
{
private WeaponSlash _weapon;
public override WeaponStateType State => WeaponStateType.Check_OutRange;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponSlash;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToTarget(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target == null || !_weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Idle);
return;
}
if (_weapon.IsInRange(_weapon._target, _weapon._sqrRange))
{
_weapon.TransitionTo(WeaponStateType.Check_InRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 89988d7be9104b0f83a423d1de2398b1
timeCreated: 1771579440
@@ -0,0 +1,32 @@
namespace Entity.Weapon
{
public partial class WeaponSlash
{
private class IdleState : WeaponStateBase
{
private WeaponSlash _weapon;
public override WeaponStateType State => WeaponStateType.Idle;
public override void OnInit(WeaponBase weapon) => _weapon = weapon as WeaponSlash;
public override void OnEnter()
{
}
public override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
_weapon.Check();
_weapon.RotateToOrigin(elapseSeconds);
_weapon._currAttackTimer += elapseSeconds;
if (_weapon._target != null && _weapon._target.Available)
{
_weapon.TransitionTo(WeaponStateType.Check_OutRange);
}
}
public override void OnLeave()
{
}
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: bec8a4a6cb974111bbd70072754f9fdb
timeCreated: 1771579452
@@ -0,0 +1,259 @@
using System.Collections.Generic;
using Definition.DataStruct;
using Definition.Enum;
using DG.Tweening;
using Entity.EntityData;
using CustomUtility;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace Entity.Weapon
{
public partial class WeaponSlash : WeaponBase
{
#region Property
private const string SectorAngleParamKey = "SectorAngle";
private WeaponSlashData _weaponData;
private Quaternion _cachedRotation;
[SerializeField] private float _rotateSpeed = 4f;
private Sequence _attackSequence;
private Vector3 _attackCenter;
private Quaternion _cachedAttackRotation;
private IWeaponAttackEffect _attackEffect;
[SerializeField] private float attackDuration = 0.4f;
[SerializeField] private float returnDuration = 0.4f;
[SerializeField] private float _swingAngleScale = 1f;
[SerializeField] private LayerMask _hitMask = ~0;
[SerializeField] private int _maxHitColliders = 32;
private Collider[] _hitResults;
private readonly HashSet<int> _hitEntityIds = new();
private float _attackRadius;
private float _attackRadiusSqr;
private float _sectorAngle;
#endregion
public override ImpactData GetImpactData()
{
return new ImpactData(_weaponData.OwnerCamp, _weaponData.Attack, AttackStat);
}
protected override void BuildStates()
{
RegisterState(new IdleState());
RegisterState(new CheckInRangeState());
RegisterState(new CheckOutRangeState());
RegisterState(new AttackState());
}
protected override void Attack()
{
StopAttackTween(false);
FaceTargetImmediately();
_isAttacking = true;
_attackCenter = CachedTransform.position;
_cachedAttackRotation = CachedTransform.rotation;
float swingAngle = Mathf.Clamp(_sectorAngle * Mathf.Max(0.1f, _swingAngleScale), 15f, 170f);
Quaternion startRotation = Quaternion.AngleAxis(-swingAngle * 0.5f, Vector3.up) * _cachedAttackRotation;
Quaternion endRotation = Quaternion.AngleAxis(swingAngle * 0.5f, Vector3.up) * _cachedAttackRotation;
CachedTransform.rotation = startRotation;
_attackSequence = DOTween.Sequence();
_attackSequence.Append(
CachedTransform.DORotateQuaternion(endRotation, attackDuration).SetEase(Ease.OutSine));
_attackSequence.InsertCallback(attackDuration * 0.5f, () =>
{
_attackEffect?.Play(this, _attackCenter, _target, _attackRadius);
ApplySectorDamage();
});
_attackSequence.Append(CachedTransform.DORotateQuaternion(_cachedAttackRotation, returnDuration)
.SetEase(Ease.InSine));
_attackSequence.AppendCallback(() =>
{
_isAttacking = false;
_attackSequence = null;
});
}
protected override void Check()
{
_target = SelectTarget(_sqrRange);
}
private void ApplySectorDamage()
{
if (_attackRadius <= 0f || _hitResults == null || _hitResults.Length == 0) return;
int hitCount = Physics.OverlapSphereNonAlloc(_attackCenter, _attackRadius, _hitResults, _hitMask,
QueryTriggerInteraction.Collide);
_hitEntityIds.Clear();
Vector3 forward = CachedTransform.forward;
forward.y = 0f;
if (forward.sqrMagnitude <= Mathf.Epsilon)
{
forward = Vector3.forward;
}
forward.Normalize();
float halfAngle = _sectorAngle * 0.5f;
for (int i = 0; i < hitCount; i++)
{
Collider collider = _hitResults[i];
if (collider == null) continue;
TargetableObject targetable = collider.GetComponentInParent<TargetableObject>();
if (targetable == null || !targetable.Available || targetable.IsDead) continue;
if (!_hitEntityIds.Add(targetable.Id)) continue;
Vector3 toTarget = targetable.CachedTransform.position - _attackCenter;
toTarget.y = 0f;
float sqrDistance = toTarget.sqrMagnitude;
if (sqrDistance <= Mathf.Epsilon || sqrDistance > _attackRadiusSqr) continue;
float angle = Vector3.Angle(forward, toTarget.normalized);
if (angle > halfAngle) continue;
AIUtility.PerformCollision(targetable, this);
}
}
private void RotateToTarget(float elapseSeconds)
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
directionToTarget.y = 0f;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
Quaternion targetRotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
CachedTransform.rotation =
Quaternion.Slerp(CachedTransform.rotation, targetRotation, _rotateSpeed * elapseSeconds);
}
private void RotateToOrigin(float elapseSeconds)
{
CachedTransform.rotation =
Quaternion.Slerp(CachedTransform.rotation, _cachedRotation, _rotateSpeed * elapseSeconds);
}
private void FaceTargetImmediately()
{
if (_target == null || !_target.Available) return;
Vector3 directionToTarget = _target.CachedTransform.position - CachedTransform.position;
directionToTarget.y = 0f;
if (directionToTarget.sqrMagnitude <= Mathf.Epsilon) return;
CachedTransform.rotation = Quaternion.LookRotation(directionToTarget.normalized, Vector3.up);
}
#region Lifecycle
protected override bool OnWeaponShow(object userData)
{
_weaponData = RequireWeaponData<WeaponSlashData>(userData);
if (_weaponData == null) return false;
WeaponData = _weaponData;
_currAttackTimer = 0f;
_sqrRange = _weaponData.AttackRange * _weaponData.AttackRange;
_cachedRotation = CachedTransform.rotation;
_attackRadius = Mathf.Max(0.1f, _weaponData.AttackRange);
_attackRadiusSqr = _attackRadius * _attackRadius;
_sectorAngle = 90f;
if (_weaponData.Params != null &&
_weaponData.Params.TryGetValue(SectorAngleParamKey.ToLower(), out string rawAngle))
{
if (float.TryParse(rawAngle, out float parsedAngle))
{
_sectorAngle = Mathf.Clamp(parsedAngle, 1f, 360f);
}
}
int capacity = Mathf.Max(1, _maxHitColliders);
if (_hitResults == null || _hitResults.Length != capacity)
{
_hitResults = new Collider[capacity];
}
_attackEffect = new SlashSectorAttackEffect();
if (_weaponData.OwnerCamp == CampType.Player)
{
gameObject.layer = LayerMask.NameToLayer("PlayerWeapon");
_hitMask = LayerMask.GetMask("Enemy");
}
else if (_weaponData.OwnerCamp == CampType.Enemy)
{
gameObject.layer = LayerMask.NameToLayer("EnemyWeapon");
_hitMask = LayerMask.GetMask("Player");
}
return true;
}
protected override void OnWeaponHide(object userData)
{
StopAttackTween(true);
_attackEffect = null;
}
protected override void OnWeaponAttach(EntityLogic parentEntity, Transform parentTransform, object userData)
{
BindAttackStatFromOwner(parentEntity);
}
protected override void OnWeaponDetach(EntityLogic parentEntity, object userData)
{
StopAttackTween(true);
ReleaseAttackStatSubscription();
}
protected override void OnEnabledChanged(bool enabled)
{
if (!enabled)
{
StopAttackTween(true);
}
}
#endregion
private void StopAttackTween(bool resetTransform)
{
if (_attackSequence != null)
{
_attackSequence.Kill();
_attackSequence = null;
}
_isAttacking = false;
if (resetTransform)
{
CachedTransform.rotation = _cachedAttackRotation;
}
_hitEntityIds.Clear();
}
public float SectorAngle => _sectorAngle;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a9d2a4fb0fc0254d85c5de012d2aff4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: