- 补充游戏核心逻辑
- 完成 TodOList P0
- 添加游戏内控制台便于调试
- 增加敌人互斥机制提高运算负载
- 优化互斥算法将游戏性能基准线拉到 1k 敌人
This commit is contained in:
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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:
|
||||
+54
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 231a935a484c4351b707b3e359074a47
|
||||
timeCreated: 1771481068
|
||||
+63
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f48328040644546bf0a866cc337d3b2
|
||||
timeCreated: 1771481100
|
||||
+86
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 329af2c156b345f28a7465ef29c943da
|
||||
timeCreated: 1771485834
|
||||
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 371bc24a18a3447686043843fdd6fbe5
|
||||
timeCreated: 1771427642
|
||||
+49
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1ed801e4816c03b4597756dc36d6aba8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+53
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 081d67a491f5792479306d88091168e3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+32
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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
|
||||
}
|
||||
}
|
||||
+11
@@ -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
|
||||
+25
@@ -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() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e72df067466464fa3be92bfd049dd0d
|
||||
timeCreated: 1771579220
|
||||
+46
@@ -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() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfd76c0fb7284ceaa28f78c702f595da
|
||||
timeCreated: 1771579231
|
||||
+33
@@ -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() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a9f936d3c8643f7bf632734548f800c
|
||||
timeCreated: 1771579241
|
||||
+27
@@ -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() { }
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -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
|
||||
+31
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 417516ffe5574e0c998105fcaa073de7
|
||||
timeCreated: 1771578955
|
||||
+49
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59644093364644768c26261b24bdb820
|
||||
timeCreated: 1771578916
|
||||
+39
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d9e1a63869694e72a360afe29cc94cfb
|
||||
timeCreated: 1771578994
|
||||
+33
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -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
|
||||
+30
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 96b136ec326941a0821f66ddf750de29
|
||||
timeCreated: 1771579419
|
||||
+48
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57a936178b5a4ff2938df369b77631e6
|
||||
timeCreated: 1771579430
|
||||
+38
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89988d7be9104b0f83a423d1de2398b1
|
||||
timeCreated: 1771579440
|
||||
+32
@@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -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:
|
||||
Reference in New Issue
Block a user