Refactor code structure for improved readability and maintainability
This commit is contained in:
8
Assets/_Game/Data/Enemies/E001.meta
Normal file
8
Assets/_Game/Data/Enemies/E001.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 16d3503453cb89640b705da44c5fbb53
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/_Game/Data/Enemies/E001/Abilities.meta
Normal file
8
Assets/_Game/Data/Enemies/E001/Abilities.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ae476df26ac83f4bb5466462f840c61
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9050afa76362dff469c64fbb48c9ff8d, type: 3}
|
||||
m_Name: ABL_E001_Alert
|
||||
m_EditorClassIdentifier:
|
||||
abilityId: e001_alert
|
||||
attackSequence: []
|
||||
cooldown: 1.5
|
||||
telegraphVfxKey:
|
||||
telegraphDuration: 0
|
||||
interruptOnHurt: 1
|
||||
interruptOnStagger: 1
|
||||
preferredMinRange: 0
|
||||
preferredMaxRange: 5
|
||||
requiresLineOfSight: 1
|
||||
requiresGrounded: 1
|
||||
exclusionGroup:
|
||||
priority: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 157dc45e6b444c64ea1a80a5886a8b92
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,27 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 9050afa76362dff469c64fbb48c9ff8d, type: 3}
|
||||
m_Name: ABL_E001_Chase
|
||||
m_EditorClassIdentifier:
|
||||
abilityId: e001_chase
|
||||
attackSequence: []
|
||||
cooldown: 1.5
|
||||
telegraphVfxKey:
|
||||
telegraphDuration: 0
|
||||
interruptOnHurt: 1
|
||||
interruptOnStagger: 1
|
||||
preferredMinRange: 0
|
||||
preferredMaxRange: 5
|
||||
requiresLineOfSight: 1
|
||||
requiresGrounded: 1
|
||||
exclusionGroup:
|
||||
priority: 0
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0adeaa8a8508fbd40986dbb71cc85acd
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/_Game/Data/Enemies/E001/ENM_E001_AnimConfig.asset
Normal file
25
Assets/_Game/Data/Enemies/E001/ENM_E001_AnimConfig.asset
Normal file
@@ -0,0 +1,25 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f7dd720bca19fcc49b22106fb65f7652, type: 3}
|
||||
m_Name: ENM_E001_AnimConfig
|
||||
m_EditorClassIdentifier:
|
||||
Idle: {fileID: 0}
|
||||
Walk: {fileID: 0}
|
||||
Run: {fileID: 0}
|
||||
Turn: {fileID: 0}
|
||||
Attack: {fileID: 0}
|
||||
Hurt: {fileID: 0}
|
||||
Stagger: {fileID: 0}
|
||||
KnockUp: {fileID: 0}
|
||||
Dead: {fileID: 0}
|
||||
Alert: {fileID: 0}
|
||||
Investigate: {fileID: 0}
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 06936c5bc3358904cb269abdfa60ed14
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
41
Assets/_Game/Data/Enemies/E001/ENM_E001_Stats.asset
Normal file
41
Assets/_Game/Data/Enemies/E001/ENM_E001_Stats.asset
Normal file
@@ -0,0 +1,41 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: ed4391dfa14c0304c8932f1ef9f8ce63, type: 3}
|
||||
m_Name: ENM_E001_Stats
|
||||
m_EditorClassIdentifier:
|
||||
MaxHP: 50
|
||||
Defense: 0
|
||||
WalkSpeed: 2
|
||||
RunSpeed: 4
|
||||
AttackDamage: 10
|
||||
AttackRange: 1.5
|
||||
AttackCooldown: 1
|
||||
DetectRange: 6
|
||||
MaxChaseDistance: 15
|
||||
LoseLinkTimeout: 2
|
||||
AlertDuration: 0.6
|
||||
InvestigateDuration: 3
|
||||
HomeRadius: 0.5
|
||||
KnockbackForce: 5
|
||||
HitStunDuration: 0.3
|
||||
HitTiers:
|
||||
heavyHitThreshold: 0
|
||||
launchThreshold: 0
|
||||
launchUpForce: 0
|
||||
launchHorzForce: 0
|
||||
knockUpDuration: 0
|
||||
EyeOffset: {x: 0, y: 0.8}
|
||||
LOSBlockingMask:
|
||||
serializedVersion: 2
|
||||
m_Bits: 1
|
||||
DetectAngleDeg: 0
|
||||
AlertBroadcastRadius: 0
|
||||
8
Assets/_Game/Data/Enemies/E001/ENM_E001_Stats.asset.meta
Normal file
8
Assets/_Game/Data/Enemies/E001/ENM_E001_Stats.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 508afd17a0cf2fe47935c78097c3b093
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -15,6 +15,7 @@
|
||||
"BaseGames.Player",
|
||||
"BaseGames.Player.States",
|
||||
"BaseGames.Enemies",
|
||||
"BaseGames.Enemies.Navigation",
|
||||
"BaseGames.Camera",
|
||||
"BaseGames.World",
|
||||
"BaseGames.UI",
|
||||
|
||||
@@ -8,6 +8,7 @@ using UnityEngine.UIElements;
|
||||
using BaseGames.Boss;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.Enemies.Abilities;
|
||||
using BaseGames.Equipment;
|
||||
using BaseGames.Input;
|
||||
using BaseGames.Parry;
|
||||
@@ -56,13 +57,24 @@ namespace BaseGames.Editor
|
||||
private List<(string label, bool exists)> _bossSOStatus = new();
|
||||
private double _lastRefreshTime;
|
||||
|
||||
// 小怪类型选择
|
||||
private int _enemyTypeIndex = 0;
|
||||
private static readonly string[] EnemyTypeLabels = { "普通(近战)", "远程", "飞行" };
|
||||
// 小怪类型选择 — 具体敌人类型
|
||||
private int _enemyTypeIndex = 0;
|
||||
private static readonly (string id, string name)[] EnemyTypes =
|
||||
{
|
||||
("E001", "草蛭"),
|
||||
("E002", "簧蛭"),
|
||||
("E003", "幼蛭"),
|
||||
("E004", "蛭母"),
|
||||
("E005", "肥蛭"),
|
||||
("E006", "讙"),
|
||||
};
|
||||
|
||||
// 动态内容区(类型切换时重建)
|
||||
private VisualElement _enemyContentArea;
|
||||
|
||||
// Boss 命名字段
|
||||
private string _bossId = "NewBoss";
|
||||
private string _enemyId = "NewEnemy";
|
||||
private string _bossId = "NewBoss"; // kept for legacy SkillSequenceSO queries if any
|
||||
private string _enemyId = "E001"; // kept for legacy status calls if any
|
||||
private string _playerId = "Player";
|
||||
|
||||
// SO 状态面板(按标签页缓存)
|
||||
@@ -202,59 +214,88 @@ namespace BaseGames.Editor
|
||||
_enemyStatusPanel = new VisualElement();
|
||||
root.Add(_enemyStatusPanel);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ 敌人类型选择"));
|
||||
root.Add(MakeSectionHeader("▶ 敌人类型"));
|
||||
root.Add(MakeHelpBox("选择要创建的具体敌人类型,对应 SO 工厂与场景放置按钮会自动切换。"));
|
||||
|
||||
var typeRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginBottom = 4 } };
|
||||
for (int i = 0; i < EnemyTypeLabels.Length; i++)
|
||||
var typeRow = new VisualElement();
|
||||
typeRow.style.flexDirection = FlexDirection.Row;
|
||||
typeRow.style.flexWrap = Wrap.Wrap;
|
||||
typeRow.style.marginBottom = 4;
|
||||
|
||||
for (int i = 0; i < EnemyTypes.Length; i++)
|
||||
{
|
||||
int captured = i;
|
||||
var (id, name) = EnemyTypes[i];
|
||||
var btn = new Button(() =>
|
||||
{
|
||||
_enemyTypeIndex = captured;
|
||||
// 高亮激活按钮(简单刷新所有同类按钮样式)
|
||||
RefreshEnemyTypeButtons(root);
|
||||
RefreshEnemyTypeButtons(typeRow);
|
||||
RefreshEnemyTabContent(_enemyContentArea);
|
||||
RefreshSOStatus();
|
||||
})
|
||||
{ text = EnemyTypeLabels[i] };
|
||||
{ text = $"{id} {name}" };
|
||||
btn.name = $"enemy-type-{i}";
|
||||
btn.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
|
||||
typeRow.Add(btn);
|
||||
}
|
||||
root.Add(typeRow);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
|
||||
root.Add(MakeHelpBox("每个敌人建议独立命名,便于 Loot / BD 资产管理。"));
|
||||
|
||||
var idRow = MakeLabeledTextField("敌人 ID", _enemyId, v => _enemyId = v);
|
||||
root.Add(idRow);
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("EnemyStatsSO", () => CreateEnemyStat()));
|
||||
factory.Add(MakeFactoryButton("LootTableSO", () => CreateLootTable()));
|
||||
factory.Add(MakeFactoryButton("AttackPatternSO × 2", () => CreateEnemyAttackPatterns()));
|
||||
factory.Add(MakeFactoryButton("DamageSourceSO", () => CreateEnemyDamageSource()));
|
||||
root.Add(factory);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
root.Add(MakeHelpBox("根据选中的类型在场景中生成对应的敌人 GameObject。"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
sceneGroup.Add(MakeSceneButton("放置敌人到场景", PlaceSelectedEnemyType));
|
||||
root.Add(sceneGroup);
|
||||
_enemyContentArea = new VisualElement();
|
||||
root.Add(_enemyContentArea);
|
||||
RefreshEnemyTabContent(_enemyContentArea);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 专项编辑器"));
|
||||
|
||||
var jumpGroup = MakeActionGroup();
|
||||
jumpGroup.Add(MakeJumpButton("Data Hub(敌人数据)", DataHubWindow.Open));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
root.Add(jumpGroup);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void RefreshEnemyTabContent(VisualElement container)
|
||||
{
|
||||
if (container == null) return;
|
||||
container.Clear();
|
||||
|
||||
var (id, name) = EnemyTypes[_enemyTypeIndex];
|
||||
string dir = $"{DataRoot}/Enemies/{id}";
|
||||
string ablDir = $"{dir}/Abilities";
|
||||
|
||||
container.Add(MakeSectionHeader($"▶ SO 资产工厂({id} {name})"));
|
||||
container.Add(MakeHelpBox($"统计 SO 路径:{dir}\n能力配置:{ablDir}"));
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton($"ENM_{id}_Stats.asset", () => { CreateEnemyStatsSO(id); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton($"ENM_{id}_AnimConfig.asset", () => { CreateEnemyAnimConfigSO(id); RefreshSOStatus(); }));
|
||||
foreach (var (ablName, ablId) in GetEnemyAbilityDefs(id))
|
||||
{
|
||||
string capturedName = ablName;
|
||||
string capturedId = ablId;
|
||||
factory.Add(MakeFactoryButton($"ABL_{id}_{capturedName}.asset",
|
||||
() => { CreateEnemyAbilitySO(id, capturedName, capturedId); RefreshSOStatus(); }));
|
||||
}
|
||||
container.Add(factory);
|
||||
|
||||
var createAllBtn = new Button(() => { CreateAllEnemySOs(id); RefreshSOStatus(); })
|
||||
{ text = $"★ 一键创建全部 {id} SO" };
|
||||
createAllBtn.AddToClassList("wizard-create-all-btn");
|
||||
container.Add(createAllBtn);
|
||||
|
||||
container.Add(MakeSeparator());
|
||||
container.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
container.Add(MakeHelpBox("在当前活动场景中放置完整组件树并自动绑定已有 SO。"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
string sceneLabel = $"放置 {id} {name} 到场景";
|
||||
sceneGroup.Add(MakeSceneButton(sceneLabel, () => PlaceSpecificEnemy(id)));
|
||||
container.Add(sceneGroup);
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// Boss 标签页
|
||||
// Boss 标签页(嘲风专属)
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private VisualElement BuildBossTab()
|
||||
@@ -265,27 +306,30 @@ namespace BaseGames.Editor
|
||||
_bossStatusPanel = new VisualElement();
|
||||
root.Add(_bossStatusPanel);
|
||||
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂"));
|
||||
root.Add(MakeHelpBox("每个 Boss 独立目录:Assets/_Game/Data/Enemies/<BossId>/"));
|
||||
|
||||
var idRow = MakeLabeledTextField("Boss ID", _bossId, v => _bossId = v);
|
||||
root.Add(idRow);
|
||||
root.Add(MakeSectionHeader("▶ SO 资产工厂(嘲风 ChaoFeng)"));
|
||||
root.Add(MakeHelpBox("路径:Assets/_Game/Data/Enemies/ChaoFeng/\n能力配置:Assets/_Game/Data/Enemies/ChaoFeng/Abilities/"));
|
||||
|
||||
var factory = MakeActionGroup();
|
||||
factory.Add(MakeFactoryButton("EnemyStatsSO(Boss)", () => CreateBossStat()));
|
||||
factory.Add(MakeFactoryButton("LootTableSO(Boss)", () => CreateBossLoot()));
|
||||
factory.Add(MakeFactoryButton("AttackPatternSO × 3(阶段)", () => CreateBossAttackPatterns()));
|
||||
factory.Add(MakeFactoryButton("BossSkillSO × 3", () => CreateBossSkills()));
|
||||
factory.Add(MakeFactoryButton("SkillSequenceSO(Phase 1)", () => CreateBossSkillSequence(1)));
|
||||
factory.Add(MakeFactoryButton("SkillSequenceSO(Phase 2)", () => CreateBossSkillSequence(2)));
|
||||
factory.Add(MakeFactoryButton("DamageSourceSO × 3", () => CreateBossDamageSources()));
|
||||
factory.Add(MakeFactoryButton("ENM_ChaoFeng_Stats.asset", () => { CreateChaoFengStatsSO(); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ENM_ChaoFeng_AnimConfig.asset",() => { CreateChaoFengAnimConfigSO(); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Idle.asset", () => { CreateChaoFengSkillSO("Idle", "chaofeng_idle"); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Slam.asset", () => { CreateChaoFengSkillSO("Slam", "chaofeng_slam"); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Sweep.asset", () => { CreateChaoFengSkillSO("Sweep", "chaofeng_sweep"); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_WindBlade.asset", () => { CreateChaoFengSkillSO("WindBlade", "chaofeng_windblade"); RefreshSOStatus(); }));
|
||||
factory.Add(MakeFactoryButton("ABL_ChaoFeng_Summon.asset", () => { CreateChaoFengSkillSO("Summon", "chaofeng_summon"); RefreshSOStatus(); }));
|
||||
root.Add(factory);
|
||||
|
||||
var createAllBtn = new Button(() => { CreateAllChaoFengSOs(); RefreshSOStatus(); })
|
||||
{ text = "★ 一键创建全部 ChaoFeng SO" };
|
||||
createAllBtn.AddToClassList("wizard-create-all-btn");
|
||||
root.Add(createAllBtn);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
root.Add(MakeSectionHeader("▶ 场景搭建"));
|
||||
root.Add(MakeHelpBox("放置嘲风完整组件树(ChaoFengBoss + 浮空控制器 + 击倒计数 + Phase1 HitBox × 4 + 炮口 × 3)。"));
|
||||
|
||||
var sceneGroup = MakeActionGroup();
|
||||
sceneGroup.Add(MakeSceneButton("放置 Boss 到场景", SceneObjectPlacerTool.PlaceBossEnemy));
|
||||
sceneGroup.Add(MakeSceneButton("放置嘲风到场景并绑定 SO", SceneObjectPlacerTool.PlaceChaoFeng));
|
||||
root.Add(sceneGroup);
|
||||
|
||||
root.Add(MakeSeparator());
|
||||
@@ -294,7 +338,7 @@ namespace BaseGames.Editor
|
||||
var jumpGroup = MakeActionGroup();
|
||||
jumpGroup.Add(MakeJumpButton("Boss 技能序列查看器", BossSkillSequenceWindow.OpenWindow));
|
||||
jumpGroup.Add(MakeJumpButton("Data Hub(Boss技能)", DataHubWindow.Open));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
jumpGroup.Add(MakeJumpButton("SO 全局校验", SOValidationRunner.ValidateMenu));
|
||||
root.Add(jumpGroup);
|
||||
|
||||
return root;
|
||||
@@ -507,96 +551,113 @@ namespace BaseGames.Editor
|
||||
EditorUtility.DisplayDialog("指定完成", msg, "确定");
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:小怪 ────────────────────────────────────────────────
|
||||
// ── SO 资产工厂:小怪(按类型) ───────────────────────────────────────────
|
||||
|
||||
private void CreateEnemyStat()
|
||||
/// <summary>返回指定敌人类型的 (abilityFileName, abilityId) 定义列表。</summary>
|
||||
private static (string ablName, string ablId)[] GetEnemyAbilityDefs(string enemyId) => enemyId switch
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_enemyId}_Stats");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
"E001" => new[] { ("Alert", "e001_alert"), ("Chase", "e001_chase") },
|
||||
"E002" => new[] { ("Strike", "e002_strike") },
|
||||
"E003" => new[] { ("Fall", "e003_fall") },
|
||||
"E004" => new[] { ("Bite", "e004_bite"), ("Slam", "e004_slam"), ("Acid", "e004_acid"),
|
||||
("Charge", "e004_charge"), ("Chase", "e004_chase") },
|
||||
"E005" => new[] { ("Bite", "e005_bite"), ("Acid", "e005_acid") },
|
||||
"E006" => new[] { ("Leap", "e006_leap"), ("Chase", "e006_chase") },
|
||||
_ => System.Array.Empty<(string, string)>(),
|
||||
};
|
||||
|
||||
private static void CreateEnemyStatsSO(string id)
|
||||
{
|
||||
string dir = $"Assets/_Game/Data/Enemies/{id}";
|
||||
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{id}_Stats");
|
||||
}
|
||||
|
||||
private void CreateLootTable()
|
||||
private static void CreateEnemyAnimConfigSO(string id)
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_enemyId}_Loot");
|
||||
if (asset != null) RefreshSOStatus();
|
||||
string dir = $"Assets/_Game/Data/Enemies/{id}";
|
||||
EditorScaffoldUtils.CreateSOAsset<EnemyAnimationConfigSO>(dir, $"ENM_{id}_AnimConfig");
|
||||
}
|
||||
|
||||
private void CreateEnemyAttackPatterns()
|
||||
private static void CreateEnemyAbilitySO(string enemyId, string ablName, string ablId)
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
foreach (var label in new[] { "Melee", "Ranged" })
|
||||
EditorScaffoldUtils.CreateSOAsset<AttackPatternSO>(dir, $"ENM_{_enemyId}_Pattern_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateEnemyDamageSource()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_enemyId}_DS");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:Boss ─────────────────────────────────────────────────
|
||||
|
||||
private void CreateBossStat()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_bossId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, $"ENM_{_bossId}_Stats");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossLoot()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_bossId}";
|
||||
EditorScaffoldUtils.CreateSOAsset<LootTableSO>(dir, $"ENM_{_bossId}_Loot");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossAttackPatterns()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_bossId}/Patterns";
|
||||
foreach (var label in new[] { "Phase1", "Phase2_A", "Phase2_B" })
|
||||
EditorScaffoldUtils.CreateSOAsset<AttackPatternSO>(dir, $"ENM_{_bossId}_Pattern_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossSkills()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_bossId}/Skills";
|
||||
foreach (var label in new[] { "Skill_Slam", "Skill_Sweep", "Skill_Summon" })
|
||||
EditorScaffoldUtils.CreateSOAsset<BossSkillSO>(dir, $"SKL_{_bossId}_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossSkillSequence(int phase)
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_bossId}/Skills";
|
||||
EditorScaffoldUtils.CreateSOAsset<SkillSequenceSO>(dir, $"SKL_{_bossId}_Phase{phase}_Sequence");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
private void CreateBossDamageSources()
|
||||
{
|
||||
string dir = $"{DataRoot}/Enemies/{_bossId}/DamageSources";
|
||||
foreach (var label in new[] { "Slam", "Sweep", "Projectile" })
|
||||
EditorScaffoldUtils.CreateSOAsset<DamageSourceSO>(dir, $"ENM_{_bossId}_DS_{label}");
|
||||
RefreshSOStatus();
|
||||
}
|
||||
|
||||
// ── 场景搭建 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void PlaceSelectedEnemyType()
|
||||
{
|
||||
switch (_enemyTypeIndex)
|
||||
string dir = $"Assets/_Game/Data/Enemies/{enemyId}/Abilities";
|
||||
string name = $"ABL_{enemyId}_{ablName}";
|
||||
var so = EditorScaffoldUtils.CreateSOAsset<EnemyAbilitySO>(dir, name);
|
||||
// Set abilityId on newly-created SO (skip if already existed = null returned)
|
||||
if (so != null)
|
||||
{
|
||||
case 0: SceneObjectPlacerTool.PlaceEnemy(); break;
|
||||
case 1: SceneObjectPlacerTool.PlaceEnemy(); break; // 复用,类型通过 SO 区分
|
||||
case 2: SceneObjectPlacerTool.PlaceEnemy(); break;
|
||||
so.abilityId = ablId;
|
||||
EditorUtility.SetDirty(so);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateAllEnemySOs(string id)
|
||||
{
|
||||
CreateEnemyStatsSO(id);
|
||||
CreateEnemyAnimConfigSO(id);
|
||||
foreach (var (ablName, ablId) in GetEnemyAbilityDefs(id))
|
||||
CreateEnemyAbilitySO(id, ablName, ablId);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog("创建完成",
|
||||
$"全部 {id} SO 已创建(已存在的跳过)。\n请放置到场景后检查组件绑定。", "确定");
|
||||
}
|
||||
|
||||
private static void PlaceSpecificEnemy(string id)
|
||||
{
|
||||
switch (id)
|
||||
{
|
||||
case "E001": SceneObjectPlacerTool.PlaceE001_CaoZhi(); break;
|
||||
case "E002": SceneObjectPlacerTool.PlaceE002_HuangZhi(); break;
|
||||
case "E003": SceneObjectPlacerTool.PlaceE003_YouZhi_Enemy(); break;
|
||||
case "E004": SceneObjectPlacerTool.PlaceE004_ZhiMu_Enemy(); break;
|
||||
case "E005": SceneObjectPlacerTool.PlaceE005_FeiZhi_Enemy(); break;
|
||||
case "E006": SceneObjectPlacerTool.PlaceE006_Huan(); break;
|
||||
default: SceneObjectPlacerTool.PlaceEnemy(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── SO 资产工厂:嘲风 Boss ─────────────────────────────────────────────
|
||||
|
||||
private static void CreateChaoFengStatsSO()
|
||||
{
|
||||
string dir = "Assets/_Game/Data/Enemies/ChaoFeng";
|
||||
EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(dir, "ENM_ChaoFeng_Stats");
|
||||
}
|
||||
|
||||
private static void CreateChaoFengAnimConfigSO()
|
||||
{
|
||||
string dir = "Assets/_Game/Data/Enemies/ChaoFeng";
|
||||
EditorScaffoldUtils.CreateSOAsset<EnemyAnimationConfigSO>(dir, "ENM_ChaoFeng_AnimConfig");
|
||||
}
|
||||
|
||||
private static void CreateChaoFengSkillSO(string skillName, string skillId)
|
||||
{
|
||||
string dir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities";
|
||||
string name = $"ABL_ChaoFeng_{skillName}";
|
||||
var so = EditorScaffoldUtils.CreateSOAsset<BossSkillSO>(dir, name);
|
||||
if (so != null)
|
||||
{
|
||||
EditorUtility.SetDirty(so);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateAllChaoFengSOs()
|
||||
{
|
||||
CreateChaoFengStatsSO();
|
||||
CreateChaoFengAnimConfigSO();
|
||||
foreach (var (n, id) in new[] { ("Idle","chaofeng_idle"), ("Slam","chaofeng_slam"),
|
||||
("Sweep","chaofeng_sweep"), ("WindBlade","chaofeng_windblade"),
|
||||
("Summon","chaofeng_summon") })
|
||||
CreateChaoFengSkillSO(n, id);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorUtility.DisplayDialog("创建完成",
|
||||
"全部嘲风 SO 已创建(已存在的跳过)。\n放置到场景后检查 BossSkillExecutor._skills 绑定。", "确定");
|
||||
}
|
||||
|
||||
// ── 场景搭建(已移至 RefreshEnemyTabContent 内的内联按钮) ─────────────
|
||||
|
||||
// ── SO 状态面板刷新 ───────────────────────────────────────────────────
|
||||
|
||||
private void RefreshSOStatus()
|
||||
@@ -636,16 +697,19 @@ namespace BaseGames.Editor
|
||||
if (_enemyStatusPanel == null) return;
|
||||
_enemyStatusPanel.Clear();
|
||||
|
||||
string dir = $"{DataRoot}/Enemies/{_enemyId}";
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
{
|
||||
("EnemyStatsSO", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{_enemyId}_Stats.asset")),
|
||||
("LootTableSO", FindAtPath<LootTableSO>($"{dir}/ENM_{_enemyId}_Loot.asset")),
|
||||
("AttackPatternSO×2", FindAtPath<AttackPatternSO>($"{dir}/ENM_{_enemyId}_Pattern_Melee.asset")),
|
||||
("DamageSourceSO", FindAtPath<DamageSourceSO>($"{dir}/ENM_{_enemyId}_DS.asset")),
|
||||
};
|
||||
var (id, name) = EnemyTypes[_enemyTypeIndex];
|
||||
string dir = $"{DataRoot}/Enemies/{id}";
|
||||
string ablDir = $"{dir}/Abilities";
|
||||
|
||||
_enemyStatusPanel.Add(MakeStatusGrid(checks));
|
||||
var items = new List<(string label, UnityEngine.Object asset)>
|
||||
{
|
||||
($"ENM_{id}_Stats", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{id}_Stats.asset")),
|
||||
($"ENM_{id}_AnimConfig",FindAtPath<EnemyAnimationConfigSO>($"{dir}/ENM_{id}_AnimConfig.asset")),
|
||||
};
|
||||
foreach (var (ablName, _) in GetEnemyAbilityDefs(id))
|
||||
items.Add(($"ABL_{id}_{ablName}", FindAtPath<EnemyAbilitySO>($"{ablDir}/ABL_{id}_{ablName}.asset")));
|
||||
|
||||
_enemyStatusPanel.Add(MakeStatusGrid(items.ToArray()));
|
||||
}
|
||||
|
||||
private void BuildBossStatus()
|
||||
@@ -653,15 +717,17 @@ namespace BaseGames.Editor
|
||||
if (_bossStatusPanel == null) return;
|
||||
_bossStatusPanel.Clear();
|
||||
|
||||
string dir = $"{DataRoot}/Enemies/{_bossId}";
|
||||
const string dir = "Assets/_Game/Data/Enemies/ChaoFeng";
|
||||
const string ablDir = "Assets/_Game/Data/Enemies/ChaoFeng/Abilities";
|
||||
var checks = new (string label, UnityEngine.Object asset)[]
|
||||
{
|
||||
("EnemyStatsSO(Boss)", FindAtPath<EnemyStatsSO>($"{dir}/ENM_{_bossId}_Stats.asset")),
|
||||
("LootTableSO", FindAtPath<LootTableSO>($"{dir}/ENM_{_bossId}_Loot.asset")),
|
||||
("AttackPatternSO(Phase1)", FindAtPath<AttackPatternSO>($"{dir}/Patterns/ENM_{_bossId}_Pattern_Phase1.asset")),
|
||||
("BossSkillSO(≥1)", EditorScaffoldUtils.FindAllAssetsOfType<BossSkillSO>()
|
||||
.FirstOrDefault(s => s.name.StartsWith("SKL_" + _bossId, StringComparison.OrdinalIgnoreCase))),
|
||||
("SkillSequenceSO(Phase1)", FindAtPath<SkillSequenceSO>($"{dir}/Skills/SKL_{_bossId}_Phase1_Sequence.asset")),
|
||||
("ENM_ChaoFeng_Stats", FindAtPath<EnemyStatsSO>($"{dir}/ENM_ChaoFeng_Stats.asset")),
|
||||
("ENM_ChaoFeng_AnimConfig",FindAtPath<EnemyAnimationConfigSO>($"{dir}/ENM_ChaoFeng_AnimConfig.asset")),
|
||||
("ABL_ChaoFeng_Idle", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Idle.asset")),
|
||||
("ABL_ChaoFeng_Slam", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Slam.asset")),
|
||||
("ABL_ChaoFeng_Sweep", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Sweep.asset")),
|
||||
("ABL_ChaoFeng_WindBlade", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_WindBlade.asset")),
|
||||
("ABL_ChaoFeng_Summon", FindAtPath<BossSkillSO>($"{ablDir}/ABL_ChaoFeng_Summon.asset")),
|
||||
};
|
||||
|
||||
_bossStatusPanel.Add(MakeStatusGrid(checks));
|
||||
@@ -782,11 +848,11 @@ namespace BaseGames.Editor
|
||||
return row;
|
||||
}
|
||||
|
||||
private void RefreshEnemyTypeButtons(VisualElement tabRoot)
|
||||
private void RefreshEnemyTypeButtons(VisualElement typeRow)
|
||||
{
|
||||
for (int i = 0; i < EnemyTypeLabels.Length; i++)
|
||||
for (int i = 0; i < EnemyTypes.Length; i++)
|
||||
{
|
||||
var btn = tabRoot.Q<Button>($"enemy-type-{i}");
|
||||
var btn = typeRow.Q<Button>($"enemy-type-{i}");
|
||||
btn?.EnableInClassList("type-btn--active", i == _enemyTypeIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Animancer;
|
||||
using BaseGames.Boss;
|
||||
using BaseGames.Camera;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Combat.StatusEffects;
|
||||
using BaseGames.Dialogue;
|
||||
using BaseGames.Enemies;
|
||||
using BaseGames.Enemies.Abilities;
|
||||
using BaseGames.Enemies.Boss;
|
||||
using BaseGames.Enemies.Navigation;
|
||||
using BaseGames.Enemies.Perception;
|
||||
using BaseGames.Equipment;
|
||||
using BaseGames.Parry;
|
||||
using BaseGames.Player;
|
||||
@@ -376,6 +381,741 @@ namespace BaseGames.Editor
|
||||
MarkDirtyAndLog("Boss Enemy", go, report);
|
||||
}
|
||||
|
||||
// ══ 具体敌人快速放置 ════════════════════════════════════════════════════
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Enemy E001 (草蛭)", priority = 111)]
|
||||
public static void PlaceE001_CaoZhi()
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E001 草蛭");
|
||||
|
||||
GameObject go = new GameObject("ENM_CaoZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E001");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "Enemy", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
rb.gravityScale = 2f;
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
|
||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
||||
body.size = new Vector2(0.6f, 0.8f);
|
||||
|
||||
GetOrAddComponent<Animator>(go);
|
||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
||||
SpriteRenderer sr1 = SetupSpriteRenderer(go);
|
||||
|
||||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<EnemyNavAgent>(go);
|
||||
GetOrAddComponent<NavAgent>(go);
|
||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
||||
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCap.isTrigger = true;
|
||||
hurtCap.size = new Vector2(0.55f, 0.75f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
|
||||
SetLayer(contactT.gameObject, "EnemyHitBox", report);
|
||||
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
|
||||
contactCol.isTrigger = true;
|
||||
contactCol.radius = 0.4f;
|
||||
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
|
||||
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
|
||||
|
||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||
Transform alertT = GetOrCreateChild(abilitiesT, "PlayClipAbility_Alert");
|
||||
PlayClipAbility alertAbility = GetOrAddComponent<PlayClipAbility>(alertT.gameObject);
|
||||
Transform chaseT = GetOrCreateChild(abilitiesT, "ContactChaseAbility_Chase");
|
||||
ContactChaseAbility chaseAbility = GetOrAddComponent<ContactChaseAbility>(chaseT.gameObject);
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E001_Stats");
|
||||
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E001_AnimConfig");
|
||||
|
||||
// Component wiring
|
||||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||||
AssignReference(enemyBase, "_movement", movement, report);
|
||||
AssignReference(enemyBase, "_animancer", animancer, report);
|
||||
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
|
||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||||
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||
|
||||
AssignAsset(movement, "_config", report, false, "ENM_E001_Stats");
|
||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E001_AnimConfig");
|
||||
AssignReference(movement, "_animancer", animancer, report);
|
||||
AssignReference(movement, "_spriteRenderer", sr1, report);
|
||||
|
||||
AssignAsset(alertAbility, "_config", report, false, "ABL_E001_Alert");
|
||||
AssignAsset(chaseAbility, "_config", report, false, "ABL_E001_Chase");
|
||||
AssignReference(chaseAbility, "_contactDamage", bodyContact, report);
|
||||
AssignReference(chaseAbility, "_sensorHub", sensorHub, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
||||
|
||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report);
|
||||
report.Add("★ 在 EnemySensorHub Inspector 中绑定 Sensor 子节点(aggro/wall_ahead/ledge)。");
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E001_CaoZhi.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("E001 草蛭", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Enemy E002 (簧蛭)", priority = 112)]
|
||||
public static void PlaceE002_HuangZhi()
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E002 簧蛭");
|
||||
|
||||
GameObject go = new GameObject("ENM_HuangZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E002");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "Enemy", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
rb.gravityScale = 0f;
|
||||
|
||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
||||
body.size = new Vector2(0.5f, 0.7f);
|
||||
|
||||
GetOrAddComponent<Animator>(go);
|
||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
||||
SetupSpriteRenderer(go);
|
||||
|
||||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
||||
|
||||
// HurtBox(初始禁用,附着天花板时不受伤)
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCap.isTrigger = true;
|
||||
hurtCap.size = new Vector2(0.45f, 0.65f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
hurtBoxT.gameObject.SetActive(false);
|
||||
|
||||
// AttackHitBox(下坠发动时由能力启用)
|
||||
Transform atkT = GetOrCreateChild(go.transform, "AttackHitBox");
|
||||
SetLayer(atkT.gameObject, "EnemyHitBox", report);
|
||||
BoxCollider2D atkCol = GetOrAddComponent<BoxCollider2D>(atkT.gameObject);
|
||||
atkCol.isTrigger = true;
|
||||
atkCol.size = new Vector2(0.5f, 0.5f);
|
||||
HitBox atkHitBox = GetOrAddComponent<HitBox>(atkT.gameObject);
|
||||
atkT.gameObject.SetActive(false);
|
||||
|
||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||
Transform strikeT = GetOrCreateChild(abilitiesT, "CeilingHangStrikeAbility");
|
||||
CeilingHangStrikeAbility strikeAbility = GetOrAddComponent<CeilingHangStrikeAbility>(strikeT.gameObject);
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E002_Stats");
|
||||
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E002_AnimConfig");
|
||||
|
||||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||||
AssignReference(enemyBase, "_animancer", animancer, report);
|
||||
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
|
||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||||
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||
|
||||
AssignAsset(strikeAbility, "_config", report, false, "ABL_E002_Strike");
|
||||
AssignReference(strikeAbility, "_attackHitBox", atkHitBox, report);
|
||||
AssignReference(strikeAbility, "_hurtBox", hurtBox, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null) AssignReference(atkHitBox, "_defaultSource", dmgSrc, report);
|
||||
|
||||
SetupSensorHubSlotNames(sensorHub, new[] { "attack_range" }, report);
|
||||
report.Add("★ 将此对象放置于天花板,调整位置使 CapsuleCollider 正好贴合天花板底面。");
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E002_HuangZhi.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("E002 簧蛭", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Enemy E003 (幼蛭)", priority = 113)]
|
||||
public static void PlaceE003_YouZhi_Enemy()
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E003 幼蛭");
|
||||
|
||||
GameObject go = new GameObject("ENM_YouZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E003");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "Enemy", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
rb.gravityScale = 0f;
|
||||
|
||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
||||
body.size = new Vector2(0.5f, 0.6f);
|
||||
|
||||
GetOrAddComponent<Animator>(go);
|
||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
||||
SpriteRenderer sr3 = SetupSpriteRenderer(go);
|
||||
|
||||
E003_YouZhi enemyBase = GetOrAddComponent<E003_YouZhi>(go);
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<EnemyNavAgent>(go);
|
||||
GetOrAddComponent<NavAgent>(go);
|
||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
||||
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCap.isTrigger = true;
|
||||
hurtCap.size = new Vector2(0.45f, 0.55f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
|
||||
SetLayer(contactT.gameObject, "EnemyHitBox", report);
|
||||
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
|
||||
contactCol.isTrigger = true;
|
||||
contactCol.radius = 0.35f;
|
||||
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
|
||||
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
|
||||
|
||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||
Transform fallT = GetOrCreateChild(abilitiesT, "AnimatedCeilingDropAbility");
|
||||
AnimatedCeilingDropAbility fallAbility = GetOrAddComponent<AnimatedCeilingDropAbility>(fallT.gameObject);
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E003_Stats");
|
||||
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E003_AnimConfig");
|
||||
|
||||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||||
AssignReference(enemyBase, "_movement", movement, report);
|
||||
AssignReference(enemyBase, "_animancer", animancer, report);
|
||||
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
|
||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||||
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||
|
||||
AssignAsset(movement, "_config", report, false, "ENM_E003_Stats");
|
||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E003_AnimConfig");
|
||||
AssignReference(movement, "_animancer", animancer, report);
|
||||
AssignReference(movement, "_spriteRenderer", sr3, report);
|
||||
|
||||
AssignAsset(fallAbility, "_config", report, false, "ABL_E003_Fall");
|
||||
AssignReference(fallAbility, "_contactDamage", bodyContact, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null) AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
||||
|
||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report);
|
||||
report.Add("★ 将此对象放置于天花板下方,E003_YouZhi 会在 OnSpawn/ActivateFromCeiling 时执行下坠。");
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E003_YouZhi.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("E003 幼蛭", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Enemy E004 (蛭母)", priority = 114)]
|
||||
public static void PlaceE004_ZhiMu_Enemy()
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E004 蛭母");
|
||||
|
||||
GameObject go = new GameObject("ENM_ZhiMu");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E004");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "Enemy", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
rb.gravityScale = 2f;
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
|
||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
||||
body.size = new Vector2(0.8f, 1.2f);
|
||||
|
||||
GetOrAddComponent<Animator>(go);
|
||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
||||
SpriteRenderer sr4 = SetupSpriteRenderer(go);
|
||||
|
||||
E004_ZhiMu enemyBase = GetOrAddComponent<E004_ZhiMu>(go);
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<EnemyNavAgent>(go);
|
||||
GetOrAddComponent<NavAgent>(go);
|
||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
||||
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCap.isTrigger = true;
|
||||
hurtCap.size = new Vector2(0.75f, 1.1f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
Transform biteT = GetOrCreateChild(go.transform, "BiteHitBox");
|
||||
SetLayer(biteT.gameObject, "EnemyHitBox", report);
|
||||
BoxCollider2D biteCol = GetOrAddComponent<BoxCollider2D>(biteT.gameObject);
|
||||
biteCol.isTrigger = true;
|
||||
biteCol.size = new Vector2(0.6f, 0.4f);
|
||||
HitBox biteHitBox = GetOrAddComponent<HitBox>(biteT.gameObject);
|
||||
biteT.gameObject.SetActive(false);
|
||||
|
||||
Transform slamT = GetOrCreateChild(go.transform, "SlamHitBox");
|
||||
SetLayer(slamT.gameObject, "EnemyHitBox", report);
|
||||
CircleCollider2D slamCol = GetOrAddComponent<CircleCollider2D>(slamT.gameObject);
|
||||
slamCol.isTrigger = true;
|
||||
slamCol.radius = 0.7f;
|
||||
HitBox slamHitBox = GetOrAddComponent<HitBox>(slamT.gameObject);
|
||||
slamT.gameObject.SetActive(false);
|
||||
|
||||
Transform chargeHitBoxT = GetOrCreateChild(go.transform, "ChargeHitBox");
|
||||
SetLayer(chargeHitBoxT.gameObject, "EnemyHitBox", report);
|
||||
BoxCollider2D chargeHitCol = GetOrAddComponent<BoxCollider2D>(chargeHitBoxT.gameObject);
|
||||
chargeHitCol.isTrigger = true;
|
||||
chargeHitCol.size = new Vector2(0.9f, 0.8f);
|
||||
HitBox chargeHitBox = GetOrAddComponent<HitBox>(chargeHitBoxT.gameObject);
|
||||
chargeHitBoxT.gameObject.SetActive(false);
|
||||
|
||||
Transform acidMuzzleT = GetOrCreateChild(go.transform, "AcidMuzzle");
|
||||
|
||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||
Transform biteAblT = GetOrCreateChild(abilitiesT, "MeleeAttackAbility_Bite");
|
||||
MeleeAttackAbility biteAbl = GetOrAddComponent<MeleeAttackAbility>(biteAblT.gameObject);
|
||||
Transform slamAblT = GetOrCreateChild(abilitiesT, "RepeatSlamAbility");
|
||||
RepeatSlamAbility slamAbl = GetOrAddComponent<RepeatSlamAbility>(slamAblT.gameObject);
|
||||
Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid");
|
||||
ProjectileAttackAbility acidAbl = GetOrAddComponent<ProjectileAttackAbility>(acidAblT.gameObject);
|
||||
Transform chargeAblT = GetOrCreateChild(abilitiesT, "ChargeAbility");
|
||||
ChargeAbility chargeAbl = GetOrAddComponent<ChargeAbility>(chargeAblT.gameObject);
|
||||
Transform chaseAblT = GetOrCreateChild(abilitiesT, "ContactChaseAbility");
|
||||
ContactChaseAbility chaseAbl = GetOrAddComponent<ContactChaseAbility>(chaseAblT.gameObject);
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E004_Stats");
|
||||
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E004_AnimConfig");
|
||||
|
||||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||||
AssignReference(enemyBase, "_movement", movement, report);
|
||||
AssignReference(enemyBase, "_animancer", animancer, report);
|
||||
AssignReference(enemyBase, "_feedback", feedback, report);
|
||||
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
|
||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||||
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||
|
||||
AssignAsset(movement, "_config", report, false, "ENM_E004_Stats");
|
||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E004_AnimConfig");
|
||||
AssignReference(movement, "_animancer", animancer, report);
|
||||
AssignReference(movement, "_spriteRenderer", sr4, report);
|
||||
|
||||
AssignAsset(biteAbl, "_config", report, false, "ABL_E004_Bite");
|
||||
AssignAsset(slamAbl, "_config", report, false, "ABL_E004_Slam");
|
||||
AssignAsset(acidAbl, "_config", report, false, "ABL_E004_Acid");
|
||||
AssignAsset(chargeAbl, "_config", report, false, "ABL_E004_Charge");
|
||||
AssignAsset(chaseAbl, "_config", report, false, "ABL_E004_Chase");
|
||||
|
||||
AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report);
|
||||
AssignReference(slamAbl, "_hitBox", slamHitBox, report);
|
||||
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
|
||||
AssignReference(chargeAbl, "_chargeHitBox", chargeHitBox, report);
|
||||
AssignReference(chaseAbl, "_sensorHub", sensorHub, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
{
|
||||
AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
|
||||
AssignReference(slamHitBox, "_defaultSource", dmgSrc, report);
|
||||
AssignReference(chargeHitBox, "_defaultSource", dmgSrc, report);
|
||||
}
|
||||
|
||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "los" }, report);
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E004_ZhiMu.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("E004 蛭母", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Enemy E005 (肥蛭)", priority = 115)]
|
||||
public static void PlaceE005_FeiZhi_Enemy()
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E005 肥蛭");
|
||||
|
||||
GameObject go = new GameObject("ENM_FeiZhi");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E005");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "Enemy", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
rb.gravityScale = 2f;
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
|
||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
||||
body.size = new Vector2(0.9f, 1.0f);
|
||||
|
||||
GetOrAddComponent<Animator>(go);
|
||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
||||
SpriteRenderer sr5 = SetupSpriteRenderer(go);
|
||||
|
||||
E005_FeiZhi enemyBase = GetOrAddComponent<E005_FeiZhi>(go);
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<EnemyNavAgent>(go);
|
||||
GetOrAddComponent<NavAgent>(go);
|
||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
||||
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCap.isTrigger = true;
|
||||
hurtCap.size = new Vector2(0.85f, 0.95f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
Transform biteT = GetOrCreateChild(go.transform, "BiteHitBox");
|
||||
SetLayer(biteT.gameObject, "EnemyHitBox", report);
|
||||
BoxCollider2D biteCol = GetOrAddComponent<BoxCollider2D>(biteT.gameObject);
|
||||
biteCol.isTrigger = true;
|
||||
biteCol.size = new Vector2(0.7f, 0.45f);
|
||||
HitBox biteHitBox = GetOrAddComponent<HitBox>(biteT.gameObject);
|
||||
biteT.gameObject.SetActive(false);
|
||||
|
||||
Transform acidMuzzleT = GetOrCreateChild(go.transform, "AcidMuzzle");
|
||||
|
||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||
Transform biteAblT = GetOrCreateChild(abilitiesT, "MeleeAttackAbility_Bite");
|
||||
MeleeAttackAbility biteAbl = GetOrAddComponent<MeleeAttackAbility>(biteAblT.gameObject);
|
||||
Transform acidAblT = GetOrCreateChild(abilitiesT, "ProjectileAttackAbility_Acid");
|
||||
ProjectileAttackAbility acidAbl = GetOrAddComponent<ProjectileAttackAbility>(acidAblT.gameObject);
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E005_Stats");
|
||||
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E005_AnimConfig");
|
||||
|
||||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||||
AssignReference(enemyBase, "_movement", movement, report);
|
||||
AssignReference(enemyBase, "_animancer", animancer, report);
|
||||
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
|
||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||||
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||
|
||||
AssignAsset(movement, "_config", report, false, "ENM_E005_Stats");
|
||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E005_AnimConfig");
|
||||
AssignReference(movement, "_animancer", animancer, report);
|
||||
AssignReference(movement, "_spriteRenderer", sr5, report);
|
||||
|
||||
AssignAsset(biteAbl, "_config", report, false, "ABL_E005_Bite");
|
||||
AssignAsset(acidAbl, "_config", report, false, "ABL_E005_Acid");
|
||||
|
||||
AssignMeleeHitBoxSlots(biteAbl, new[] { ("bite", biteHitBox) }, report);
|
||||
AssignReference(acidAbl, "_muzzle", acidMuzzleT, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null) AssignReference(biteHitBox, "_defaultSource", dmgSrc, report);
|
||||
|
||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro" }, report);
|
||||
report.Add("★ 在 E005_FeiZhi._deathPreClip 上添加 AnimationEvent 调用 SpawnProjectile(\"spawn_e003\")。");
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E005_FeiZhi.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("E005 肥蛭", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Enemy E006 (讙)", priority = 116)]
|
||||
public static void PlaceE006_Huan()
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place E006 讙");
|
||||
|
||||
GameObject go = new GameObject("ENM_Huan");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place E006");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "Enemy", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
rb.gravityScale = 2f;
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
|
||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
||||
body.size = new Vector2(0.7f, 1.0f);
|
||||
|
||||
GetOrAddComponent<Animator>(go);
|
||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
||||
SpriteRenderer sr6 = SetupSpriteRenderer(go);
|
||||
|
||||
EnemyBase enemyBase = GetOrAddComponent<EnemyBase>(go);
|
||||
EnemyStats enemyStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<EnemyNavAgent>(go);
|
||||
GetOrAddComponent<NavAgent>(go);
|
||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
||||
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCap.isTrigger = true;
|
||||
hurtCap.size = new Vector2(0.65f, 0.95f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
Transform contactT = GetOrCreateChild(go.transform, "ContactDamageZone");
|
||||
SetLayer(contactT.gameObject, "EnemyHitBox", report);
|
||||
CircleCollider2D contactCol = GetOrAddComponent<CircleCollider2D>(contactT.gameObject);
|
||||
contactCol.isTrigger = true;
|
||||
contactCol.radius = 0.4f;
|
||||
HitBox contactHitBox = GetOrAddComponent<HitBox>(contactT.gameObject);
|
||||
BodyContactDamage bodyContact = GetOrAddComponent<BodyContactDamage>(contactT.gameObject);
|
||||
|
||||
Transform landT = GetOrCreateChild(go.transform, "LandingHitBox");
|
||||
SetLayer(landT.gameObject, "EnemyHitBox", report);
|
||||
CircleCollider2D landCol = GetOrAddComponent<CircleCollider2D>(landT.gameObject);
|
||||
landCol.isTrigger = true;
|
||||
landCol.radius = 0.8f;
|
||||
HitBox landHitBox = GetOrAddComponent<HitBox>(landT.gameObject);
|
||||
landT.gameObject.SetActive(false);
|
||||
|
||||
Transform abilitiesT = GetOrCreateChild(go.transform, "Abilities");
|
||||
Transform leapT = GetOrCreateChild(abilitiesT, "LeapAttackAbility");
|
||||
LeapAttackAbility leapAbl = GetOrAddComponent<LeapAttackAbility>(leapT.gameObject);
|
||||
Transform chaseT = GetOrCreateChild(abilitiesT, "ContactChaseAbility");
|
||||
ContactChaseAbility chaseAbl = GetOrAddComponent<ContactChaseAbility>(chaseT.gameObject);
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(enemyBase, "_statsSO", report, false, "ENM_E006_Stats");
|
||||
AssignAsset(enemyBase, "_animConfig", report, false, "ENM_E006_AnimConfig");
|
||||
|
||||
AssignReference(enemyBase, "_stats", enemyStats, report);
|
||||
AssignReference(enemyBase, "_movement", movement, report);
|
||||
AssignReference(enemyBase, "_animancer", animancer, report);
|
||||
AssignReference(enemyBase, "_hurtBox", hurtBox, report);
|
||||
AssignAsset(enemyBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
AssignAsset(enemyBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||||
AssignAsset(enemyStats, "_onDifficultyChanged", report, false, "EVT_DifficultyChanged");
|
||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||
|
||||
AssignAsset(movement, "_config", report, false, "ENM_E006_Stats");
|
||||
AssignAsset(movement, "_animConfig", report, false, "ENM_E006_AnimConfig");
|
||||
AssignReference(movement, "_animancer", animancer, report);
|
||||
AssignReference(movement, "_spriteRenderer", sr6, report);
|
||||
|
||||
AssignAsset(leapAbl, "_config", report, false, "ABL_E006_Leap");
|
||||
AssignAsset(chaseAbl, "_config", report, false, "ABL_E006_Chase");
|
||||
|
||||
AssignReference(leapAbl, "_landingHitBox", landHitBox, report);
|
||||
AssignReference(chaseAbl, "_contactDamage", bodyContact, report);
|
||||
AssignReference(chaseAbl, "_sensorHub", sensorHub, report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
{
|
||||
AssignReference(contactHitBox, "_defaultSource", dmgSrc, report);
|
||||
AssignReference(landHitBox, "_defaultSource", dmgSrc, report);
|
||||
}
|
||||
|
||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "wall_ahead", "ledge" }, report);
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 E006_Huan.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("E006 讙", go, report);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Boss 嘲风 (ChaoFeng)", priority = 117)]
|
||||
public static void PlaceChaoFeng()
|
||||
{
|
||||
var report = new List<string>();
|
||||
int undoGroup = Undo.GetCurrentGroup();
|
||||
Undo.SetCurrentGroupName("Place Boss 嘲风");
|
||||
|
||||
GameObject go = new GameObject("ENM_ChaoFeng");
|
||||
Undo.RegisterCreatedObjectUndo(go, "Place ChaoFeng");
|
||||
go.transform.position = GetDropPosition();
|
||||
SetLayer(go, "Enemy", report);
|
||||
|
||||
Rigidbody2D rb = GetOrAddComponent<Rigidbody2D>(go);
|
||||
rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
rb.gravityScale = 2f;
|
||||
rb.constraints = RigidbodyConstraints2D.FreezeRotation;
|
||||
rb.collisionDetectionMode = CollisionDetectionMode2D.Continuous;
|
||||
rb.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||
|
||||
CapsuleCollider2D body = GetOrAddComponent<CapsuleCollider2D>(go);
|
||||
body.size = new Vector2(1.2f, 2.0f);
|
||||
|
||||
GetOrAddComponent<Animator>(go);
|
||||
AnimancerComponent animancer = GetOrAddComponent<AnimancerComponent>(go);
|
||||
SpriteRenderer srBoss = SetupSpriteRenderer(go);
|
||||
|
||||
ChaoFengBoss bossBase = GetOrAddComponent<ChaoFengBoss>(go);
|
||||
EnemyStats bossStats = GetOrAddComponent<EnemyStats>(go);
|
||||
EnemyFeedback feedback = GetOrAddComponent<EnemyFeedback>(go);
|
||||
EnemyMovement movement = GetOrAddComponent<EnemyMovement>(go);
|
||||
GetOrAddComponent<EnemyNavAgent>(go);
|
||||
GetOrAddComponent<NavAgent>(go);
|
||||
BossSkillExecutor skillExec = GetOrAddComponent<BossSkillExecutor>(go);
|
||||
ChaoFengFloatController floatCtrl = GetOrAddComponent<ChaoFengFloatController>(go);
|
||||
ChaoFengKnockdownCounter knockdown = GetOrAddComponent<ChaoFengKnockdownCounter>(go);
|
||||
EnemySensorHub sensorHub = GetOrAddComponent<EnemySensorHub>(go);
|
||||
|
||||
// HurtBox
|
||||
Transform hurtBoxT = GetOrCreateChild(go.transform, "HurtBox");
|
||||
SetLayer(hurtBoxT.gameObject, "EnemyHurtBox", report);
|
||||
CapsuleCollider2D hurtCap = GetOrAddComponent<CapsuleCollider2D>(hurtBoxT.gameObject);
|
||||
hurtCap.isTrigger = true;
|
||||
hurtCap.size = new Vector2(1.1f, 1.9f);
|
||||
HurtBox hurtBox = GetOrAddComponent<HurtBox>(hurtBoxT.gameObject);
|
||||
|
||||
// Phase1 attack hitboxes (disabled by default; abilities enable/disable as needed)
|
||||
HitBox biteHB = CreateDisabledHitBox(go.transform, "Phase1_BiteHitBox", "EnemyHitBox",
|
||||
true, report, size: new Vector2(0.8f, 0.5f));
|
||||
HitBox swipeR = CreateDisabledHitBox(go.transform, "Phase1_SwipeHitBox_R","EnemyHitBox",
|
||||
true, report, size: new Vector2(1.2f, 0.4f));
|
||||
HitBox swipeL = CreateDisabledHitBox(go.transform, "Phase1_SwipeHitBox_L","EnemyHitBox",
|
||||
true, report, size: new Vector2(1.2f, 0.4f));
|
||||
HitBox stompHB = CreateDisabledHitBox(go.transform, "Phase1_StompHitBox", "EnemyHitBox",
|
||||
false, report, radius: 1.0f);
|
||||
|
||||
// Muzzle transforms for Phase 2 skills
|
||||
GetOrCreateChild(go.transform, "WindBladeMuzzle");
|
||||
GetOrCreateChild(go.transform, "TornadoMuzzle");
|
||||
GetOrCreateChild(go.transform, "SummonSpawnPoint");
|
||||
|
||||
// SOs — assign first so OnValidate doesn't warn during wiring
|
||||
AssignAsset(bossBase, "_statsSO", report, false, "ENM_ChaoFeng_Stats");
|
||||
AssignAsset(bossBase, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
|
||||
|
||||
// Component wiring
|
||||
AssignReference(bossBase, "_stats", bossStats, report);
|
||||
AssignReference(bossBase, "_movement", movement, report);
|
||||
AssignReference(bossBase, "_animancer", animancer, report);
|
||||
AssignReference(bossBase, "_feedback", feedback, report);
|
||||
AssignReference(bossBase, "_hurtBox", hurtBox, report);
|
||||
AssignReference(skillExec, "_animancer", animancer, report);
|
||||
|
||||
AssignAsset(bossBase, "_onEnemyDied", report, false, "EVT_EnemyDied");
|
||||
AssignAsset(bossBase, "_onPlayerSpawned", report, false, "EVT_PlayerSpawned");
|
||||
AssignAsset(bossBase, "_onBossFightEnded", report, false, "EVT_BossFightEnded");
|
||||
AssignAsset(bossBase, "_onBossPhaseChanged", report, false, "EVT_BossPhaseChanged");
|
||||
AssignAsset(bossStats, "_onDifficultyChanged",report, false, "EVT_DifficultyChanged");
|
||||
AssignAsset(hurtBox, "_onDamageDealt", report, false, "EVT_DamageDealt");
|
||||
AssignAsset(hurtBox, "_onHitConfirmed", report, false, "EVT_HitConfirmed");
|
||||
|
||||
AssignAsset(movement, "_config", report, false, "ENM_ChaoFeng_Stats");
|
||||
AssignAsset(movement, "_animConfig", report, false, "ENM_ChaoFeng_AnimConfig");
|
||||
AssignReference(movement, "_animancer", animancer, report);
|
||||
AssignReference(movement, "_spriteRenderer", srBoss, report);
|
||||
|
||||
// Collect BossSkillSOs and assign to executor
|
||||
var skillAssets = new System.Collections.Generic.List<Object>();
|
||||
foreach (var n in new[] { "ABL_ChaoFeng_Idle", "ABL_ChaoFeng_Slam", "ABL_ChaoFeng_Sweep",
|
||||
"ABL_ChaoFeng_WindBlade", "ABL_ChaoFeng_Summon" })
|
||||
{
|
||||
Object sk = FindFirstAsset(n);
|
||||
if (sk != null) skillAssets.Add(sk);
|
||||
else report.Add($"未找到 BossSkillSO:{n},请先一键创建 SO 后再重新运行此放置操作。");
|
||||
}
|
||||
if (skillAssets.Count > 0)
|
||||
AssignObjectArray(skillExec, "_skills", skillAssets.ToArray(), report);
|
||||
|
||||
Object dmgSrc = FindFirstAsset("CMB_DS_BossBody", "CMB_DS_EnemyBody");
|
||||
if (dmgSrc != null)
|
||||
{
|
||||
foreach (var hb in new[] { biteHB, swipeR, swipeL, stompHB })
|
||||
if (hb != null) AssignReference(hb, "_defaultSource", dmgSrc, report);
|
||||
}
|
||||
|
||||
SetupSensorHubSlotNames(sensorHub, new[] { "aggro", "attack_melee", "attack_range", "los" }, report);
|
||||
|
||||
report.Add("★ 设置 BossSkillExecutor._bossId = \"ChaoFeng\"。");
|
||||
report.Add("★ 将各 Phase1 HitBox 引用拖入 BossSkillExecutor._hitBoxes 数组。");
|
||||
report.Add("★ 将 WindBladeMuzzle / TornadoMuzzle / SummonSpawnPoint 拖入对应 BossSkillSO 字段。");
|
||||
report.Add("★ 挂载行为树 BehaviorTree 组件,指定 Boss_ChaoFeng.asset。");
|
||||
|
||||
Undo.CollapseUndoOperations(undoGroup);
|
||||
Selection.activeGameObject = go;
|
||||
MarkDirtyAndLog("Boss 嘲风 (ChaoFeng)", go, report);
|
||||
}
|
||||
|
||||
// ══ 敌人放置辅助方法 ═══════════════════════════════════════════════════
|
||||
|
||||
/// <summary>
|
||||
/// 创建禁用的 HitBox 子节点(Box 或 Circle 碰撞体)。
|
||||
/// </summary>
|
||||
private static HitBox CreateDisabledHitBox(Transform parent, string childName, string layer,
|
||||
bool isBox, List<string> report,
|
||||
Vector2 size = default, float radius = 0.5f)
|
||||
{
|
||||
Transform t = GetOrCreateChild(parent, childName);
|
||||
SetLayer(t.gameObject, layer, report);
|
||||
if (isBox)
|
||||
{
|
||||
BoxCollider2D col = GetOrAddComponent<BoxCollider2D>(t.gameObject);
|
||||
col.isTrigger = true;
|
||||
col.size = size == default ? new Vector2(0.5f, 0.5f) : size;
|
||||
}
|
||||
else
|
||||
{
|
||||
CircleCollider2D col = GetOrAddComponent<CircleCollider2D>(t.gameObject);
|
||||
col.isTrigger = true;
|
||||
col.radius = radius;
|
||||
}
|
||||
HitBox hb = GetOrAddComponent<HitBox>(t.gameObject);
|
||||
t.gameObject.SetActive(false);
|
||||
return hb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 EnemySensorHub._slots 的 slotName 字段(Sensor 引用需在 Inspector 中手工绑定)。
|
||||
/// </summary>
|
||||
private static void SetupSensorHubSlotNames(EnemySensorHub hub, string[] slotNames, List<string> report)
|
||||
{
|
||||
var so = new SerializedObject(hub);
|
||||
var slots = so.FindProperty("_slots");
|
||||
if (slots == null || !slots.isArray)
|
||||
{
|
||||
report?.Add("EnemySensorHub._slots 字段未找到,传感器槽位需手工配置。");
|
||||
return;
|
||||
}
|
||||
slots.arraySize = slotNames.Length;
|
||||
for (int i = 0; i < slotNames.Length; i++)
|
||||
slots.GetArrayElementAtIndex(i).FindPropertyRelative("slotName").stringValue = slotNames[i];
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Scene/Place/Hazard (LethalTrap)", priority = 120)]
|
||||
public static void PlaceLethalTrap()
|
||||
{
|
||||
@@ -1084,6 +1824,30 @@ namespace BaseGames.Editor
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 MeleeAttackAbility._hitBoxSlots 赋值(struct 数组 {slotName, hitBox})。
|
||||
/// </summary>
|
||||
private static void AssignMeleeHitBoxSlots(MeleeAttackAbility ability, (string slot, HitBox hb)[] slots, List<string> report)
|
||||
{
|
||||
if (ability == null) return;
|
||||
var so = new SerializedObject(ability);
|
||||
var prop = so.FindProperty("_hitBoxSlots");
|
||||
if (prop == null || !prop.isArray)
|
||||
{
|
||||
report?.Add($"[WARN] MeleeAttackAbility._hitBoxSlots 属性未找到,请检查字段名。");
|
||||
return;
|
||||
}
|
||||
prop.arraySize = slots.Length;
|
||||
for (int i = 0; i < slots.Length; i++)
|
||||
{
|
||||
var elem = prop.GetArrayElementAtIndex(i);
|
||||
elem.FindPropertyRelative("slotName").stringValue = slots[i].slot;
|
||||
elem.FindPropertyRelative("hitBox").objectReferenceValue = slots[i].hb;
|
||||
}
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
report?.Add($"[OK] MeleeAttackAbility._hitBoxSlots 已配置 {slots.Length} 个槽位。");
|
||||
}
|
||||
|
||||
private static Object FindFirstAsset(params string[] candidates)
|
||||
{
|
||||
foreach (string candidate in candidates)
|
||||
|
||||
@@ -10,11 +10,12 @@ namespace BaseGames.Enemies.AI
|
||||
/// BD Action:启动 Boss 阶段过渡演出(无敌帧 + 可选定格),等待过渡完成后返回 Success。
|
||||
///
|
||||
/// 返回 Running:过渡演出进行中(BossBase.IsPhaseTransitioning = true)。
|
||||
/// 返回 Success:过渡完成,已切换到目标阶段。
|
||||
/// 返回 Success:过渡完成或目标阶段已达到(含守护逻辑,防止重复触发)。
|
||||
/// 返回 Failure:BossBase 组件不存在。
|
||||
///
|
||||
/// 典型 BT 用法:
|
||||
/// Sequence [ BD_IsHPBelow(0.5) → BD_BossPhaseTransition(Phase=1, Duration=1.5) → ... ]
|
||||
/// Sequence [ BD_IsHPBelow(0.5) → BD_BossPhaseTransition(Phase=1, Duration=1.5) → Phase2战斗节点 ]
|
||||
/// 过渡完成后,此节点每 tick 因守护立即返回 Success,Sequence 直接进入后续战斗节点。
|
||||
/// </summary>
|
||||
[TaskName("Boss Phase Transition")]
|
||||
[TaskCategory("BaseGames/Enemy/Boss")]
|
||||
@@ -41,6 +42,9 @@ namespace BaseGames.Enemies.AI
|
||||
{
|
||||
if (_boss == null) return TaskStatus.Failure;
|
||||
|
||||
// 防止 BT 重入时重复触发阶段过渡:目标阶段已达到则直接返回 Success
|
||||
if (_boss.CurrentPhase >= m_TargetPhase) return TaskStatus.Success;
|
||||
|
||||
if (!_started)
|
||||
{
|
||||
_boss.BeginPhaseTransition(m_TargetPhase, m_InvincibleDuration);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 带动画的天花板跌落能力:播放 Fall 动画 + 切换物理体为 Dynamic 自由下落,落地后启用接触伤害并恢复巡逻。
|
||||
/// 取代 sealed 的 CeilingDropAbility,动画所有权完整封装在本能力中。
|
||||
/// 可复用于任何需要从天花板落下并造成接触伤害的敌人。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
public class AnimatedCeilingDropAbility : EnemyAbilityBase
|
||||
{
|
||||
[Header("动画")]
|
||||
[Tooltip("下落循环动画(旋转下落姿态,循环播放直到落地)")]
|
||||
[SerializeField] private ClipTransition _fallLoopClip;
|
||||
|
||||
[Header("物理下落")]
|
||||
[Tooltip("切换 Dynamic 后使用的重力倍率")]
|
||||
[SerializeField] private float _fallGravityScale = 3.5f;
|
||||
[Tooltip("下落超时保护(秒),超时后强制继续执行")]
|
||||
[SerializeField] private float _maxFallTime = 3f;
|
||||
[SerializeField] private LayerMask _groundMask;
|
||||
|
||||
[Header("落地后")]
|
||||
[Tooltip("落地后恢复帧延迟(秒),之后切换为 Patrol 阶段")]
|
||||
[SerializeField] private float _recoveryTime = 0.1f;
|
||||
[SerializeField] private BodyContactDamage _contactDamage;
|
||||
|
||||
private Rigidbody2D _rb;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
_rb = GetComponentInParent<Rigidbody2D>();
|
||||
}
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
|
||||
// 播放下落动画(能力脚本负责动画所有权)
|
||||
if (_fallLoopClip.Clip != null)
|
||||
_animancer.Play(_fallLoopClip);
|
||||
|
||||
// 切换物理:Kinematic → Dynamic + 重力
|
||||
var origBodyType = _rb.bodyType;
|
||||
var origGravScale = _rb.gravityScale;
|
||||
_rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
_rb.gravityScale = _fallGravityScale;
|
||||
_rb.velocity = Vector2.zero;
|
||||
|
||||
// 等待落地(超时保护)
|
||||
float elapsed = 0f;
|
||||
while (elapsed < _maxFallTime)
|
||||
{
|
||||
elapsed += Time.fixedDeltaTime;
|
||||
yield return new WaitForFixedUpdate();
|
||||
if (elapsed > 0.05f && IsGrounded()) break;
|
||||
}
|
||||
_rb.velocity = Vector2.zero;
|
||||
|
||||
// 落地后启用接触伤害
|
||||
if (_contactDamage != null)
|
||||
_contactDamage.enabled = true;
|
||||
|
||||
yield return EnemyAbilityWaits.Get(_recoveryTime);
|
||||
|
||||
// SetAiPhase(Patrol) 自动播放 AnimConfig.Walk(地面移动动画)
|
||||
_enemy.SetAiPhase(AiPhase.Patrol);
|
||||
}
|
||||
|
||||
private bool IsGrounded()
|
||||
{
|
||||
var hit = Physics2D.Raycast(_rb.position, Vector2.down, 0.6f, _groundMask);
|
||||
return hit.collider != null;
|
||||
}
|
||||
|
||||
protected override void OnInterrupted(InterruptReason reason)
|
||||
{
|
||||
if (_rb != null)
|
||||
_rb.velocity = Vector2.zero;
|
||||
if (_contactDamage != null)
|
||||
_contactDamage.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76c9c3969851d084396edd617bac8ef0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Assets/_Game/Scripts/Enemies/Abilities/AppearAbility.cs
Normal file
25
Assets/_Game/Scripts/Enemies/Abilities/AppearAbility.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 出场演出能力:播放出场动画后切换到 Combat 阶段(适用于小Boss或需出场动画的精英怪)。
|
||||
/// 可复用于任何"播出场动画→进入战斗"的敌人。
|
||||
/// </summary>
|
||||
public class AppearAbility : EnemyAbilityBase
|
||||
{
|
||||
[SerializeField] private ClipTransition _appearClip;
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
if (_appearClip.Clip == null) yield break;
|
||||
|
||||
_animancer.Play(_appearClip);
|
||||
yield return EnemyAbilityWaits.Get(_appearClip.Clip.length);
|
||||
_enemy.SetAiPhase(AiPhase.Combat);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/Abilities/AppearAbility.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/Abilities/AppearAbility.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 419edb00543abc047a8e7601cf0f3ac4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 天花板三段攻击能力:出击 → 脆弱悬挂窗口 → 收回。
|
||||
/// 悬挂阶段(_loopClip)为脆弱窗口:HurtBox 激活,等待 _hangDuration 后结束。
|
||||
/// 可复用于任何固定位置的天花板攻击型敌人。
|
||||
/// </summary>
|
||||
public class CeilingHangStrikeAbility : EnemyAbilityBase
|
||||
{
|
||||
[Header("动画")]
|
||||
[SerializeField] private ClipTransition _strikeClip;
|
||||
[SerializeField] private ClipTransition _loopClip;
|
||||
[SerializeField] private ClipTransition _endClip;
|
||||
|
||||
[Header("碰撞")]
|
||||
[SerializeField] private HitBox _attackHitBox;
|
||||
[SerializeField] private HurtBox _hurtBox;
|
||||
|
||||
[Header("行为")]
|
||||
[Tooltip("脆弱悬挂窗口持续时间(秒)")]
|
||||
[SerializeField] private float _hangDuration = 2f;
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
|
||||
// 出击阶段:播放攻击动画并激活 HitBox
|
||||
if (_strikeClip.Clip != null)
|
||||
{
|
||||
_animancer.Play(_strikeClip);
|
||||
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
|
||||
_attackHitBox?.Activate(dmgSrc, _transform);
|
||||
yield return EnemyAbilityWaits.Get(_strikeClip.Clip.length);
|
||||
}
|
||||
_attackHitBox?.Deactivate();
|
||||
|
||||
// 脆弱悬挂阶段:HurtBox 激活为玩家反击窗口
|
||||
if (_loopClip.Clip != null)
|
||||
_animancer.Play(_loopClip);
|
||||
|
||||
if (_hurtBox != null)
|
||||
_hurtBox.enabled = true;
|
||||
|
||||
yield return EnemyAbilityWaits.Get(_hangDuration);
|
||||
|
||||
if (_hurtBox != null)
|
||||
_hurtBox.enabled = false;
|
||||
|
||||
// 收回阶段
|
||||
if (_endClip.Clip != null)
|
||||
{
|
||||
_animancer.Play(_endClip);
|
||||
yield return EnemyAbilityWaits.Get(_endClip.Clip.length);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInterrupted(InterruptReason reason)
|
||||
{
|
||||
_attackHitBox?.Deactivate();
|
||||
if (_hurtBox != null)
|
||||
_hurtBox.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5615d1d3eb70f414fb137428a3f7f962
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using UnityEngine;
|
||||
using BaseGames.Enemies.Perception;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 循环追击 + 体接触伤害能力。
|
||||
/// 追击期间每帧更新 MoveTo,失去感知后收招退出并恢复巡逻状态。
|
||||
/// 可复用于任何需要"追击+接触伤害循环"的敌人。
|
||||
/// </summary>
|
||||
public class ContactChaseAbility : EnemyAbilityBase
|
||||
{
|
||||
[Header("动画")]
|
||||
[SerializeField] private ClipTransition _loopClip;
|
||||
[SerializeField] private ClipTransition _endClip;
|
||||
|
||||
[Header("感知与接触伤害")]
|
||||
[SerializeField] private BodyContactDamage _contactDamage;
|
||||
[SerializeField] private EnemySensorHub _sensorHub;
|
||||
|
||||
[Tooltip("用于追击感知判断的传感器槽位名,通常为 \"aggro\"")]
|
||||
[SerializeField] private string _aggroSlotName = "aggro";
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
|
||||
if (_loopClip.Clip != null)
|
||||
_animancer.Play(_loopClip);
|
||||
|
||||
if (_contactDamage != null)
|
||||
_contactDamage.enabled = true;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (_enemy.PlayerTransform == null) break;
|
||||
if (_sensorHub != null && !_sensorHub.IsDetecting(_aggroSlotName, _enemy.PlayerTransform.gameObject))
|
||||
break;
|
||||
_enemy.MoveTo(_enemy.PlayerTransform.position);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (_contactDamage != null)
|
||||
_contactDamage.enabled = false;
|
||||
|
||||
_enemy.StopMovement();
|
||||
|
||||
if (_endClip.Clip != null)
|
||||
{
|
||||
_animancer.Play(_endClip);
|
||||
yield return EnemyAbilityWaits.Get(_endClip.Clip.length);
|
||||
}
|
||||
|
||||
_enemy.SetAiPhase(AiPhase.Patrol);
|
||||
}
|
||||
|
||||
protected override void OnInterrupted(InterruptReason reason)
|
||||
{
|
||||
if (_contactDamage != null)
|
||||
_contactDamage.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7bfd6f44ebdb5bf489ab6703b1ee429b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
42
Assets/_Game/Scripts/Enemies/Abilities/FacePlayerAbility.cs
Normal file
42
Assets/_Game/Scripts/Enemies/Abilities/FacePlayerAbility.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
using Animancer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 面向玩家能力:朝向玩家后播放转身/翻转动画并等待完成。
|
||||
/// CanUse 重写:仅在无其他技能运行且玩家在背后时可用。
|
||||
/// 可复用于任何需要"检测背后玩家并翻转"的敌人。
|
||||
/// </summary>
|
||||
public class FacePlayerAbility : EnemyAbilityBase
|
||||
{
|
||||
[SerializeField] private ClipTransition _faceClip;
|
||||
|
||||
public override bool CanUse =>
|
||||
base.CanUse
|
||||
&& !_enemy.Abilities.All.Any(a => a != this && a.IsRunning)
|
||||
&& IsPlayerBehind();
|
||||
|
||||
private bool IsPlayerBehind()
|
||||
{
|
||||
if (_enemy.PlayerTransform == null) return false;
|
||||
float dx = _enemy.PlayerTransform.position.x - _enemy.transform.position.x;
|
||||
int facing = _enemy.Movement?.FacingDirection ?? 1;
|
||||
return (facing > 0 && dx < 0) || (facing < 0 && dx > 0);
|
||||
}
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
|
||||
_enemy.Movement?.FaceTarget(_enemy.PlayerTransform.position);
|
||||
|
||||
if (_faceClip.Clip == null) yield break;
|
||||
|
||||
_animancer.Play(_faceClip);
|
||||
yield return EnemyAbilityWaits.Get(_faceClip.Clip.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c97df5c477ad0b488171b2b39530b08
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 近战攻击带后摇脆弱窗口能力:攻击动画期间按时间比例激活 HitBox,
|
||||
/// 攻击完成后开放 HurtBox(脆弱窗口),窗口结束后恢复正常。
|
||||
/// 可复用于任何"攻击后有反击窗口"的敌人技能。
|
||||
/// </summary>
|
||||
public class MeleeVulnerabilityAbility : EnemyAbilityBase
|
||||
{
|
||||
[Header("动画")]
|
||||
[SerializeField] private ClipTransition _attackClip;
|
||||
|
||||
[Header("碰撞")]
|
||||
[SerializeField] private HitBox _hitBox;
|
||||
[SerializeField] private HurtBox _hurtBox;
|
||||
|
||||
[Header("打击时间(0~1 归一化,基于 Clip 时长)")]
|
||||
[SerializeField, Range(0f, 1f)] private float _hitEnterT = 0.30f;
|
||||
[SerializeField, Range(0f, 1f)] private float _hitExitT = 0.60f;
|
||||
|
||||
[Header("后摇脆弱窗口")]
|
||||
[Tooltip("HurtBox 激活时间(秒)")]
|
||||
[SerializeField] private float _staggerDuration = 1.0f;
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
if (_attackClip.Clip == null) yield break;
|
||||
|
||||
float len = _attackClip.Clip.length;
|
||||
float t = 0f;
|
||||
bool active = false;
|
||||
|
||||
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
|
||||
|
||||
_animancer.Play(_attackClip);
|
||||
|
||||
while (t < len)
|
||||
{
|
||||
t += Time.deltaTime;
|
||||
|
||||
if (!active && t >= len * _hitEnterT)
|
||||
{
|
||||
_hitBox?.Activate(dmgSrc, _transform);
|
||||
active = true;
|
||||
}
|
||||
if (active && t >= len * _hitExitT)
|
||||
{
|
||||
_hitBox?.Deactivate();
|
||||
active = false;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (active)
|
||||
_hitBox?.Deactivate();
|
||||
|
||||
// 后摇脆弱窗口
|
||||
if (_hurtBox != null)
|
||||
_hurtBox.enabled = true;
|
||||
|
||||
yield return EnemyAbilityWaits.Get(_staggerDuration);
|
||||
|
||||
if (_hurtBox != null)
|
||||
_hurtBox.enabled = false;
|
||||
}
|
||||
|
||||
protected override void OnInterrupted(InterruptReason reason)
|
||||
{
|
||||
_hitBox?.Deactivate();
|
||||
if (_hurtBox != null)
|
||||
_hurtBox.enabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5fc3af69bdd62a418fe6e2ce136c9a8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
24
Assets/_Game/Scripts/Enemies/Abilities/PlayClipAbility.cs
Normal file
24
Assets/_Game/Scripts/Enemies/Abilities/PlayClipAbility.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 播放单个动画 Clip 并等待完成后退出(无副作用)。
|
||||
/// 可复用于任何需要"播一段动画即完成"的能力(如出场激活、台词演出等)。
|
||||
/// </summary>
|
||||
public class PlayClipAbility : EnemyAbilityBase
|
||||
{
|
||||
[SerializeField] private ClipTransition _clip;
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
if (_clip.Clip == null) yield break;
|
||||
|
||||
_animancer.Play(_clip);
|
||||
yield return EnemyAbilityWaits.Get(_clip.Clip.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a26fca0fa72894a4da1a5a58ee023154
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
75
Assets/_Game/Scripts/Enemies/Abilities/RepeatSlamAbility.cs
Normal file
75
Assets/_Game/Scripts/Enemies/Abilities/RepeatSlamAbility.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Abilities
|
||||
{
|
||||
/// <summary>
|
||||
/// 多段砸地能力:起手 → 循环砸地(N次,带 HitBox 激活)→ 收招。
|
||||
/// 支持霸体(ABL SO 配置 interruptOnHurt=false),后摇时间可配置。
|
||||
/// 可复用于任何多段地面攻击型敌人。
|
||||
/// </summary>
|
||||
public class RepeatSlamAbility : EnemyAbilityBase
|
||||
{
|
||||
[Header("动画")]
|
||||
[SerializeField] private ClipTransition _startClip;
|
||||
[SerializeField] private ClipTransition _loopClip;
|
||||
[SerializeField] private ClipTransition _endClip;
|
||||
|
||||
[Header("打击")]
|
||||
[SerializeField] private HitBox _hitBox;
|
||||
|
||||
[Header("行为配置")]
|
||||
[Tooltip("每次砸地时 HitBox 激活时长(秒)")]
|
||||
[SerializeField] private float _hitActiveTime = 0.15f;
|
||||
[Tooltip("砸地次数")]
|
||||
[SerializeField] private int _slamCount = 2;
|
||||
[Tooltip("最后一次砸地收招后额外等待时间(秒)")]
|
||||
[SerializeField] private float _staggerDuration = 1.2f;
|
||||
|
||||
protected override IEnumerator ExecuteCoroutine()
|
||||
{
|
||||
Phase = AbilityRunState.Active;
|
||||
|
||||
if (_startClip.Clip != null)
|
||||
{
|
||||
_animancer.Play(_startClip);
|
||||
yield return EnemyAbilityWaits.Get(_startClip.Clip.length);
|
||||
}
|
||||
|
||||
for (int i = 0; i < _slamCount; i++)
|
||||
{
|
||||
if (_loopClip.Clip != null)
|
||||
{
|
||||
_animancer.Play(_loopClip);
|
||||
float preHit = Mathf.Max(0f, _loopClip.Clip.length - _hitActiveTime - 0.05f);
|
||||
yield return EnemyAbilityWaits.Get(preHit);
|
||||
}
|
||||
|
||||
var dmgSrc = _config?.attackSequence?.Length > 0 ? _config.attackSequence[0].damageSource : null;
|
||||
_hitBox?.Activate(dmgSrc, _transform);
|
||||
yield return EnemyAbilityWaits.Get(_hitActiveTime);
|
||||
_hitBox?.Deactivate();
|
||||
|
||||
if (i < _slamCount - 1)
|
||||
yield return EnemyAbilityWaits.Get(0.1f);
|
||||
}
|
||||
|
||||
if (_endClip.Clip != null)
|
||||
{
|
||||
_animancer.Play(_endClip);
|
||||
yield return EnemyAbilityWaits.Get(_endClip.Clip.length + _staggerDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return EnemyAbilityWaits.Get(_staggerDuration);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInterrupted(InterruptReason reason)
|
||||
{
|
||||
_hitBox?.Deactivate();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff71de2a91f535d488c76df6395f1d78
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -214,6 +214,7 @@ namespace BaseGames.Enemies
|
||||
private IEnumerator PhaseTransitionCoroutine(int targetPhase, float duration)
|
||||
{
|
||||
IsPhaseTransitioning = true;
|
||||
OnBeginPhaseTransition(targetPhase);
|
||||
|
||||
// 打断技能 + 停止移动
|
||||
_skillExecutor?.InterruptCurrentSkill();
|
||||
@@ -232,8 +233,7 @@ namespace BaseGames.Enemies
|
||||
_phaseTransitionCoroutine = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即终止阶段过渡协程并清除标志位。
|
||||
/// <summary>立即终止阶段过渡协程并清除标志位。
|
||||
/// 死亡时调用,防止 IsPhaseTransitioning 永久为 true 影响对象池复用。
|
||||
/// </summary>
|
||||
private void AbortPhaseTransition()
|
||||
@@ -246,6 +246,12 @@ namespace BaseGames.Enemies
|
||||
IsPhaseTransitioning = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 阶段过渡开始时回调(子类可重写以触发演出动画或特殊逻辑)。
|
||||
/// 在无敌帧等待之前调用。
|
||||
/// </summary>
|
||||
protected virtual void OnBeginPhaseTransition(int targetPhase) { }
|
||||
|
||||
/// <summary>检查当前 HP 是否低于指定百分比(0~1)。</summary>
|
||||
public bool IsHPBelow(float ratio)
|
||||
{
|
||||
|
||||
171
Assets/_Game/Scripts/Enemies/Boss/ChaoFengBoss.cs
Normal file
171
Assets/_Game/Scripts/Enemies/Boss/ChaoFengBoss.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Pool;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace BaseGames.Enemies.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 嘲风 Boss 主脚本。
|
||||
///
|
||||
/// Phase 0(地面):4 技能加权随机(回旋扇/扇形连击/小龙卷/大龙卷)。
|
||||
/// Phase 1(空中):风石技能 + 击落计数机制。
|
||||
///
|
||||
/// 阶段过渡流程:
|
||||
/// 1. BossBase.BeginPhaseTransition → OnBeginPhaseTransition(1) 立即播放过渡动画 + 开始浮空
|
||||
/// 2. 无敌期结束(≥ _riseDuration+buffer)→ BossBase.EnterPhase(1) 广播阶段切换事件
|
||||
///
|
||||
/// ⚠️ 动画由本脚本通过 Animancer.Play() 完整控制,不在 BD 中调用 BD_PlayAnimation。
|
||||
/// </summary>
|
||||
public class ChaoFengBoss : BossBase
|
||||
{
|
||||
[Header("浮空 / 击落")]
|
||||
[SerializeField] private ChaoFengFloatController _floatController;
|
||||
[SerializeField] private ChaoFengKnockdownCounter _knockdownCounter;
|
||||
|
||||
[Header("阶段过渡动画")]
|
||||
[SerializeField] private ClipTransition _phaseTransitionClip;
|
||||
|
||||
[Header("回旋扇收招动画")]
|
||||
[SerializeField] private ClipTransition _boomerangEndClip;
|
||||
|
||||
[Header("弹体发射点")]
|
||||
[SerializeField] private Transform _boomerangMuzzle;
|
||||
[SerializeField] private Transform _tornadoMuzzle;
|
||||
[SerializeField] private Transform _windStoneMuzzle;
|
||||
|
||||
[Header("击败演出动画")]
|
||||
[SerializeField] private ClipTransition _defeatStruggleClip;
|
||||
[Tooltip("倒地喘气(循环);与 ChaoFengKnockdownCounter._staggerClip 共用同一 Clip")]
|
||||
[SerializeField] private ClipTransition _defeatPantClip;
|
||||
[SerializeField] private ClipTransition _defeatStandUpClip;
|
||||
[SerializeField] private float _defeatPantDuration = 3f;
|
||||
|
||||
[Header("白屏回调(可接 CameraManager / VFX Event)")]
|
||||
[SerializeField] private UnityEngine.Events.UnityEvent _onDefeatWhiteFlash;
|
||||
|
||||
// ── 阶段过渡钩子 ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 阶段过渡开始时立即播放过渡动画并启动浮空协程。
|
||||
/// invincibleDuration 需在 BD_BossPhaseTransition 中配置为 ≥ _riseDuration + 缓冲(约 2.0s)。
|
||||
/// </summary>
|
||||
protected override void OnBeginPhaseTransition(int targetPhase)
|
||||
{
|
||||
if (targetPhase == 1)
|
||||
{
|
||||
if (_phaseTransitionClip.Clip != null)
|
||||
Animancer.Play(_phaseTransitionClip);
|
||||
StartCoroutine(_floatController.FloatUp());
|
||||
}
|
||||
}
|
||||
|
||||
// ── 受击转发 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 转发受击事件至击落计数器。
|
||||
/// ⚠️ HurtBox 无公开 OnDamageTaken 事件,必须通过此虚方法转发。
|
||||
/// </summary>
|
||||
protected override void OnDamageTaken(DamageInfo info)
|
||||
{
|
||||
base.OnDamageTaken(info);
|
||||
_knockdownCounter?.OnBossHit(info);
|
||||
}
|
||||
|
||||
// ── 动画事件 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 回旋扇返回时由 ReturnProjectile 调用,触发收扇动画。
|
||||
/// </summary>
|
||||
public void OnBoomerangReturned()
|
||||
{
|
||||
if (_boomerangEndClip.Clip != null)
|
||||
Animancer.Play(_boomerangEndClip);
|
||||
}
|
||||
|
||||
// ── 弹体生成 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 由技能动画 AnimationEvent 触发,生成对应弹体。
|
||||
/// payload: "boomerang" / "tornado_small" / "tornado_large" / "wind_stone"
|
||||
/// </summary>
|
||||
public override void SpawnProjectile(string payload)
|
||||
{
|
||||
var pool = ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||||
if (pool == null) return;
|
||||
|
||||
switch (payload)
|
||||
{
|
||||
case "boomerang":
|
||||
{
|
||||
var go = pool.Spawn("PROJ_Boomerang",
|
||||
_boomerangMuzzle != null ? _boomerangMuzzle.position : transform.position,
|
||||
Quaternion.identity);
|
||||
go?.GetComponent<ReturnProjectile>()?.SetOwner(transform);
|
||||
break;
|
||||
}
|
||||
case "tornado_small":
|
||||
pool.Spawn("PROJ_TornadoSmall",
|
||||
_tornadoMuzzle != null ? _tornadoMuzzle.position : transform.position,
|
||||
Quaternion.identity);
|
||||
break;
|
||||
|
||||
case "tornado_large":
|
||||
if (PlayerTransform != null)
|
||||
pool.Spawn("PROJ_TornadoLarge", PlayerTransform.position, Quaternion.identity);
|
||||
break;
|
||||
|
||||
case "wind_stone":
|
||||
pool.Spawn("PROJ_WindStone",
|
||||
_windStoneMuzzle != null ? _windStoneMuzzle.position : transform.position,
|
||||
Quaternion.identity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 击败演出 ─────────────────────────────────────────────────────────
|
||||
|
||||
protected override void Die()
|
||||
{
|
||||
StartCoroutine(DefeatSequence());
|
||||
}
|
||||
|
||||
private IEnumerator DefeatSequence()
|
||||
{
|
||||
StopBehaviorTree();
|
||||
_knockdownCounter?.ForceEnd();
|
||||
|
||||
// Phase 2(空中)先落地
|
||||
if (CurrentPhase >= 1 && _floatController != null)
|
||||
yield return _floatController.FallDown();
|
||||
|
||||
// 空中挣扎(Defeat_Struggle)
|
||||
if (_defeatStruggleClip.Clip != null)
|
||||
{
|
||||
Animancer.Play(_defeatStruggleClip);
|
||||
yield return new WaitForSeconds(_defeatStruggleClip.Clip.length);
|
||||
}
|
||||
|
||||
// 白屏效果
|
||||
_onDefeatWhiteFlash?.Invoke();
|
||||
|
||||
// 倒地喘气(Defeat_Pant 循环)
|
||||
if (_defeatPantClip.Clip != null)
|
||||
Animancer.Play(_defeatPantClip);
|
||||
yield return new WaitForSeconds(_defeatPantDuration);
|
||||
|
||||
// 站起(Defeat_StandUp 单次)
|
||||
if (_defeatStandUpClip.Clip != null)
|
||||
{
|
||||
Animancer.Play(_defeatStandUpClip);
|
||||
yield return new WaitForSeconds(_defeatStandUpClip.Clip.length);
|
||||
}
|
||||
|
||||
// 广播战斗结束、触发结算过场
|
||||
base.Die();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/Boss/ChaoFengBoss.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/Boss/ChaoFengBoss.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7eec9978ded028a409537e37d029c8cd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
46
Assets/_Game/Scripts/Enemies/Boss/ChaoFengFloatController.cs
Normal file
46
Assets/_Game/Scripts/Enemies/Boss/ChaoFengFloatController.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 嘲风漂浮控制器:负责 Phase 2 进入时上浮、击落时下落的平滑位移。
|
||||
/// 使用 Rigidbody2D.MovePosition 做帧级 Lerp 插值,避免物理穿墙。
|
||||
/// </summary>
|
||||
public class ChaoFengFloatController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float _floatHeight = 5f;
|
||||
[SerializeField] private float _riseDuration = 1.5f;
|
||||
[SerializeField] private float _fallDuration = 0.8f;
|
||||
[SerializeField] private Rigidbody2D _rb;
|
||||
|
||||
private float _groundY;
|
||||
|
||||
private void Start() => _groundY = transform.position.y;
|
||||
|
||||
/// <summary>上浮至悬空高度(切换为 Kinematic 后执行)。</summary>
|
||||
public IEnumerator FloatUp()
|
||||
{
|
||||
_rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
_rb.velocity = Vector2.zero;
|
||||
yield return TweenY(transform.position.y, _groundY + _floatHeight, _riseDuration);
|
||||
}
|
||||
|
||||
/// <summary>下落回地面(落地后恢复 Dynamic)。</summary>
|
||||
public IEnumerator FallDown()
|
||||
{
|
||||
yield return TweenY(transform.position.y, _groundY, _fallDuration);
|
||||
_rb.bodyType = RigidbodyType2D.Dynamic;
|
||||
}
|
||||
|
||||
private IEnumerator TweenY(float from, float to, float duration)
|
||||
{
|
||||
for (float t = 0f; t < duration; t += Time.deltaTime)
|
||||
{
|
||||
_rb.MovePosition(new Vector2(transform.position.x, Mathf.Lerp(from, to, t / duration)));
|
||||
yield return null;
|
||||
}
|
||||
_rb.MovePosition(new Vector2(transform.position.x, to));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 29caa23156f932e43b02015182e66819
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using BaseGames.Boss;
|
||||
using BaseGames.Combat;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 嘲风击落计数器(Phase 2)。
|
||||
/// 由 <see cref="ChaoFengBoss.OnDamageTaken"/> 直接调用 <see cref="OnBossHit"/>,
|
||||
/// 累计命中达到阈值后触发击落序列(下落 → 硬直 → 复位浮空)。
|
||||
///
|
||||
/// ⚠️ HurtBox 无公开 OnDamageTaken 事件,必须通过 EnemyBase.OnDamageTaken 虚方法转发。
|
||||
/// </summary>
|
||||
public class ChaoFengKnockdownCounter : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private int _threshold = 8;
|
||||
|
||||
[Header("依赖引用")]
|
||||
[SerializeField] private ChaoFengBoss _boss;
|
||||
[SerializeField] private ChaoFengFloatController _floatCtrl;
|
||||
|
||||
[Header("击落动画")]
|
||||
[SerializeField] private ClipTransition _knockdownHitClip;
|
||||
[Tooltip("击落后硬直动画(复用 Defeat_Pant Clip)")]
|
||||
[SerializeField] private ClipTransition _staggerClip;
|
||||
|
||||
[SerializeField] private float _staggerDuration = 3f;
|
||||
|
||||
private int _count;
|
||||
private bool _inKnockdown;
|
||||
|
||||
/// <summary>
|
||||
/// 由 <see cref="ChaoFengBoss.OnDamageTaken"/> 调用,累计受击并在达到阈值时触发击落。
|
||||
/// </summary>
|
||||
public void OnBossHit(DamageInfo info)
|
||||
{
|
||||
if (_inKnockdown || _boss == null || _boss.CurrentPhase != 1) return;
|
||||
|
||||
_count++;
|
||||
if (_count >= _threshold)
|
||||
{
|
||||
_count = 0;
|
||||
StartCoroutine(KnockdownSequence());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>强制结束正在进行中的击落序列(由 ChaoFengBoss.DefeatSequence 调用)。</summary>
|
||||
public void ForceEnd()
|
||||
{
|
||||
StopAllCoroutines();
|
||||
_inKnockdown = false;
|
||||
_count = 0;
|
||||
}
|
||||
|
||||
private IEnumerator KnockdownSequence()
|
||||
{
|
||||
_inKnockdown = true;
|
||||
|
||||
_boss.GetComponentInChildren<BossSkillExecutor>()?.InterruptCurrentSkill();
|
||||
|
||||
if (_knockdownHitClip.Clip != null)
|
||||
_boss.Animancer.Play(_knockdownHitClip);
|
||||
|
||||
yield return _floatCtrl.FallDown();
|
||||
|
||||
if (_staggerClip.Clip != null)
|
||||
_boss.Animancer.Play(_staggerClip);
|
||||
|
||||
yield return new WaitForSeconds(_staggerDuration);
|
||||
|
||||
yield return _floatCtrl.FloatUp();
|
||||
|
||||
_inKnockdown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e0b39336c1ec5740abb2754b75bde04
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
61
Assets/_Game/Scripts/Enemies/Boss/ReturnProjectile.cs
Normal file
61
Assets/_Game/Scripts/Enemies/Boss/ReturnProjectile.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using UnityEngine;
|
||||
using BaseGames.Combat;
|
||||
|
||||
namespace BaseGames.Enemies.Boss
|
||||
{
|
||||
/// <summary>
|
||||
/// 嘲风回旋扇弹体:向前飞行至最大射程后自动追踪 Boss 返回。
|
||||
/// 命中目标或返回到 Boss 身边时归还对象池,并通知 Boss 触发收扇动画。
|
||||
/// </summary>
|
||||
public class ReturnProjectile : Projectile
|
||||
{
|
||||
private enum Stage { Forward, Returning }
|
||||
|
||||
[SerializeField] private float _maxRange = 8f;
|
||||
[SerializeField] private float _returnSpeed = 6f;
|
||||
|
||||
private Stage _stage;
|
||||
private Transform _ownerTransform;
|
||||
private Vector2 _startPos;
|
||||
|
||||
/// <summary>设置持有者 Transform(发射时由 ChaoFengBoss.SpawnProjectile 调用)。</summary>
|
||||
public void SetOwner(Transform owner) => _ownerTransform = owner;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_stage = Stage.Forward;
|
||||
_startPos = transform.position;
|
||||
_rb.velocity = Direction * _config.Speed;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
// 不使用基类的 Lifetime 自毁;由射程 / 返回逻辑控制生命周期
|
||||
if (_stage == Stage.Forward)
|
||||
{
|
||||
if (Vector2.Distance(transform.position, _startPos) >= _maxRange)
|
||||
{
|
||||
_stage = Stage.Returning;
|
||||
_rb.velocity = Vector2.zero;
|
||||
}
|
||||
}
|
||||
else // Returning
|
||||
{
|
||||
if (_ownerTransform == null)
|
||||
{
|
||||
base.ReturnToPool();
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 dir = ((Vector2)_ownerTransform.position - (Vector2)transform.position).normalized;
|
||||
_rb.velocity = dir * _returnSpeed;
|
||||
|
||||
if (Vector2.Distance(transform.position, _ownerTransform.position) < 0.5f)
|
||||
{
|
||||
_ownerTransform.GetComponentInParent<ChaoFengBoss>()?.OnBoomerangReturned();
|
||||
base.ReturnToPool();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/Boss/ReturnProjectile.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/Boss/ReturnProjectile.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4b3290cb8ac8a2f42a10c7c0c845c8ef
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
34
Assets/_Game/Scripts/Enemies/E003_YouZhi.cs
Normal file
34
Assets/_Game/Scripts/Enemies/E003_YouZhi.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// E003 幼蛭。HP=1,一击即死。
|
||||
/// 支持双路初始化:
|
||||
/// - 预置路径:场景战斗触发器调用 <see cref="ActivateFromCeiling"/>
|
||||
/// - 对象池路径:E005 死亡时通过 <see cref="OnSpawn"/> 自动触发
|
||||
/// 能力脚本 <c>AnimatedCeilingDropAbility</c> 负责 Fall 动画 + 物理下落 + SetAiPhase(Patrol)。
|
||||
/// </summary>
|
||||
public class E003_YouZhi : EnemyBase
|
||||
{
|
||||
[Tooltip("对象池生成时是否立即执行下落能力(E005 触发生成路径)")]
|
||||
[SerializeField] private bool _activateOnSpawn = true;
|
||||
|
||||
public override void OnSpawn()
|
||||
{
|
||||
base.OnSpawn();
|
||||
if (_activateOnSpawn)
|
||||
Abilities.Get("e003_fall")?.Execute();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 场景预置路径:由场景触发器(EventTrigger / Animator Event)调用,触发天花板跌落。
|
||||
/// </summary>
|
||||
public void ActivateFromCeiling()
|
||||
{
|
||||
Abilities.Get("e003_fall")?.Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/E003_YouZhi.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/E003_YouZhi.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d86a36c2999f88842a212d095749c349
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
45
Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs
Normal file
45
Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// E004 蛭母(小Boss)。
|
||||
/// 特性:
|
||||
/// - 出场演出(AppearAbility)
|
||||
/// - 三技能(撕咬/头槌/酸液)+ 翻身(FacePlayerAbility)
|
||||
/// - 死亡两阶段:Death_Pre 无敌演出 → base.Die()
|
||||
/// </summary>
|
||||
public class E004_ZhiMu : EnemyBase
|
||||
{
|
||||
[Header("死亡演出")]
|
||||
[SerializeField] private ClipTransition _deathPreClip;
|
||||
[SerializeField] private HurtBox _hurtBox;
|
||||
[SerializeField] private float _deathPreDuration = 3f;
|
||||
|
||||
protected override void Die()
|
||||
{
|
||||
StartCoroutine(DeathSequence());
|
||||
}
|
||||
|
||||
private IEnumerator DeathSequence()
|
||||
{
|
||||
// 停止行为树防止覆盖演出动画
|
||||
StopBehaviorTree();
|
||||
StopMovement();
|
||||
|
||||
if (_hurtBox != null)
|
||||
_hurtBox.enabled = false;
|
||||
|
||||
if (_deathPreClip.Clip != null)
|
||||
{
|
||||
Animancer.Play(_deathPreClip);
|
||||
yield return new WaitForSeconds(_deathPreDuration);
|
||||
}
|
||||
|
||||
base.Die();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/E004_ZhiMu.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf8f8c7225dca9c42b5a451b177319b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
66
Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs
Normal file
66
Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using BaseGames.Combat;
|
||||
using BaseGames.Core;
|
||||
using BaseGames.Core.Pool;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// E005 肥蛭(精英怪)。
|
||||
/// 特性:
|
||||
/// - 撕咬(MeleeVulnerabilityAbility)含后摇脆弱窗口
|
||||
/// - 死亡两阶段:Death_Pre 无敌演出(含 AnimationEvent spawn_e003)→ base.Die()
|
||||
/// </summary>
|
||||
public class E005_FeiZhi : EnemyBase
|
||||
{
|
||||
[Header("死亡演出")]
|
||||
[SerializeField] private ClipTransition _deathPreClip;
|
||||
[SerializeField] private HurtBox _hurtBox;
|
||||
[SerializeField] private float _deathPreDuration = 3f;
|
||||
|
||||
[Header("生成 E003(Death_Pre AnimationEvent 触发)")]
|
||||
[SerializeField] private int _spawnCount = 3;
|
||||
[SerializeField] private float _spawnRadius = 1.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Death_Pre 动画适当帧的 AnimationEvent 调用 SpawnProjectile("spawn_e003") 生成幼蛭。
|
||||
/// </summary>
|
||||
public override void SpawnProjectile(string payload)
|
||||
{
|
||||
if (payload != "spawn_e003") return;
|
||||
|
||||
var pool = BaseGames.Core.ServiceLocator.GetOrDefault<IObjectPoolService>();
|
||||
if (pool == null) return;
|
||||
|
||||
for (int i = 0; i < _spawnCount; i++)
|
||||
{
|
||||
Vector2 offset = Random.insideUnitCircle * _spawnRadius;
|
||||
pool.Spawn("ENM_YouZhi", (Vector2)transform.position + offset, Quaternion.identity);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Die()
|
||||
{
|
||||
StartCoroutine(DeathSequence());
|
||||
}
|
||||
|
||||
private IEnumerator DeathSequence()
|
||||
{
|
||||
StopBehaviorTree();
|
||||
StopMovement();
|
||||
|
||||
if (_hurtBox != null)
|
||||
_hurtBox.enabled = false;
|
||||
|
||||
if (_deathPreClip.Clip != null)
|
||||
{
|
||||
Animancer.Play(_deathPreClip);
|
||||
yield return new WaitForSeconds(_deathPreDuration);
|
||||
}
|
||||
|
||||
base.Die();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs.meta
Normal file
11
Assets/_Game/Scripts/Enemies/E005_FeiZhi.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2460e8735a4dc5409fe6b0949bd65c0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -13,6 +13,8 @@ namespace BaseGames.Enemies
|
||||
public AnimationClip Idle;
|
||||
public AnimationClip Walk;
|
||||
public AnimationClip Run;
|
||||
[Tooltip("转身动画(可选);配合 EnemyMovement._enableTurnAnimation 使用,留空则瞬时翻转")]
|
||||
public AnimationClip Turn;
|
||||
|
||||
[Header("战斗")]
|
||||
public AnimationClip Attack;
|
||||
@@ -41,6 +43,7 @@ namespace BaseGames.Enemies
|
||||
"Idle" => Idle,
|
||||
"Walk" => Walk,
|
||||
"Run" or "Chase" => Run,
|
||||
"Turn" => Turn,
|
||||
"Attack" or "Attack_Melee" => Attack,
|
||||
"Hurt" => Hurt,
|
||||
"Stagger" => Stagger,
|
||||
|
||||
@@ -616,6 +616,17 @@ namespace BaseGames.Enemies
|
||||
private float _btCurrentInterval;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 停止行为树(子类 Die() 预演出阶段可调用,防止 BT 继续 Tick 覆盖演出逻辑)。
|
||||
/// 内部使用 #if GRAPH_DESIGNER 保护,子类无需处理条件编译。
|
||||
/// </summary>
|
||||
protected void StopBehaviorTree()
|
||||
{
|
||||
#if GRAPH_DESIGNER
|
||||
_behaviorTree?.StopBehavior();
|
||||
#endif
|
||||
}
|
||||
|
||||
protected virtual void Die()
|
||||
{
|
||||
if (_currentState == EnemyStateType.Dead) return;
|
||||
@@ -702,7 +713,7 @@ namespace BaseGames.Enemies
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
if (_statsSO == null)
|
||||
Debug.LogError($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置,运行时会 NullRef。", this);
|
||||
Debug.LogWarning($"[EnemyBase] {gameObject.name} 缺少 EnemyStatsSO 配置(运行时会 NullRef)。", this);
|
||||
if (_stats == null)
|
||||
Debug.LogWarning($"[EnemyBase] {gameObject.name} 未绑定 EnemyStats 组件引用。", this);
|
||||
if (_animancer == null)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using Animancer;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Enemies
|
||||
@@ -22,6 +23,14 @@ namespace BaseGames.Enemies
|
||||
[SerializeField] private EnemyStatsSO _config;
|
||||
[SerializeField] private SpriteRenderer _spriteRenderer;
|
||||
|
||||
[Header("转身动画")]
|
||||
[Tooltip("开启后,敌人翻转方向时播放转身动画并暂停水平移动,动画结束后完成翻转")]
|
||||
[SerializeField] private bool _enableTurnAnimation = false;
|
||||
[Tooltip("Animancer 组件引用;留空则在 Awake 时自动从父级查找")]
|
||||
[SerializeField] private AnimancerComponent _animancer;
|
||||
[Tooltip("动画配置 SO;留空则在 Awake 时自动从 EnemyBase 读取")]
|
||||
[SerializeField] private EnemyAnimationConfigSO _animConfig;
|
||||
|
||||
[Header("导航跳跃能力(INavLinkHandler)")]
|
||||
[Tooltip("可处理的最大跳跃垂直高度(超出则让 TBM 兜底)")]
|
||||
[SerializeField] private float _navJumpMaxHeight = 6f;
|
||||
@@ -36,9 +45,16 @@ namespace BaseGames.Enemies
|
||||
private int _facingDir = 1;
|
||||
private Coroutine _linkCoroutine;
|
||||
|
||||
// ── 转身状态 ────────────────────────────────────────────────────────
|
||||
private bool _isTurning;
|
||||
private int _pendingFacingDir; // 转身目标方向,转身完成后 ApplyFacingFlip 使用
|
||||
private Coroutine _turnCoroutine;
|
||||
|
||||
public bool IsGrounded { get; private set; }
|
||||
/// <summary>当前朝向:1 = 右,-1 = 左。</summary>
|
||||
public int FacingDirection => _facingDir;
|
||||
/// <summary>当前是否正在播放转身动画(移动输入在此期间被屏蔽)。</summary>
|
||||
public bool IsTurning => _isTurning;
|
||||
|
||||
// ── INavLinkHandler ────────────────────────────────────────────
|
||||
private static readonly NavLinkType[] _handledTypes =
|
||||
@@ -59,6 +75,7 @@ namespace BaseGames.Enemies
|
||||
|
||||
public void BeginLinkTraversal(NavLinkType type, Vector2 linkStart, Vector2 linkEnd, Action onComplete)
|
||||
{
|
||||
CancelTurn(); // 进入连接段前中止任何进行中的转身
|
||||
if (_linkCoroutine != null) StopCoroutine(_linkCoroutine);
|
||||
_linkCoroutine = type == NavLinkType.Jump
|
||||
? StartCoroutine(JumpLinkCoroutine(linkStart, linkEnd, onComplete))
|
||||
@@ -68,6 +85,7 @@ namespace BaseGames.Enemies
|
||||
public void AbortLinkTraversal()
|
||||
{
|
||||
if (_linkCoroutine != null) { StopCoroutine(_linkCoroutine); _linkCoroutine = null; }
|
||||
CancelTurn();
|
||||
StopHorizontal();
|
||||
}
|
||||
|
||||
@@ -117,6 +135,16 @@ namespace BaseGames.Enemies
|
||||
{
|
||||
Debug.Assert(_config != null, "[EnemyMovement] _config 未赋值,请在 Prefab Inspector 中指定 EnemyStatsSO。", this);
|
||||
_rb = GetComponent<Rigidbody2D>();
|
||||
|
||||
if (_enableTurnAnimation)
|
||||
{
|
||||
if (_animancer == null) _animancer = GetComponentInParent<AnimancerComponent>(true);
|
||||
if (_animConfig == null)
|
||||
{
|
||||
var enemyBase = GetComponentInParent<EnemyBase>(true);
|
||||
if (enemyBase != null) _animConfig = enemyBase.AnimConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
@@ -124,30 +152,49 @@ namespace BaseGames.Enemies
|
||||
IsGrounded = IsGroundedCheck();
|
||||
}
|
||||
|
||||
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。</summary>
|
||||
/// <summary>按 SO 配置速度水平移动。dir: +1 右 / -1 左 / 0 停止。转身动画期间调用无效。</summary>
|
||||
public void MoveHorizontal(float dir)
|
||||
{
|
||||
if (_isTurning) return;
|
||||
var vel = _rb.velocity;
|
||||
vel.x = dir * _config.WalkSpeed;
|
||||
_rb.velocity = vel;
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
/// <summary>显式指定速度(BD 追击任务调用)。</summary>
|
||||
/// <summary>显式指定速度(BD 追击任务调用)。转身动画期间调用无效。</summary>
|
||||
public void MoveWithSpeed(float dir, float speed)
|
||||
{
|
||||
if (_isTurning) return;
|
||||
var vel = _rb.velocity;
|
||||
vel.x = dir * speed;
|
||||
_rb.velocity = vel;
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
/// <summary>朝向指定世界坐标(通常传入玩家位置)。</summary>
|
||||
public void FaceTarget(Vector2 targetPos)
|
||||
{
|
||||
float dir = targetPos.x < transform.position.x ? -1f : 1f;
|
||||
UpdateFacing(dir);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 直接指定朝向方向。dir: +1 = 右,-1 = 左。
|
||||
/// 若启用转身动画且方向确实改变,会触发转身流程。
|
||||
/// </summary>
|
||||
public void FaceDirection(int dir)
|
||||
{
|
||||
if (dir == 0) return;
|
||||
UpdateFacing(dir > 0 ? 1f : -1f);
|
||||
}
|
||||
|
||||
/// <summary>朝向右方(+X)。</summary>
|
||||
public void FaceRight() => FaceDirection(1);
|
||||
|
||||
/// <summary>朝向左方(-X)。</summary>
|
||||
public void FaceLeft() => FaceDirection(-1);
|
||||
|
||||
public void ApplyKnockback(Vector2 dir, float force)
|
||||
{
|
||||
_rb.velocity = dir.normalized * force;
|
||||
@@ -197,16 +244,68 @@ namespace BaseGames.Enemies
|
||||
private void UpdateFacing(float dir)
|
||||
{
|
||||
if (Mathf.Approximately(dir, 0f)) return;
|
||||
if (_isTurning) return; // 转身进行中,忽略新的朝向请求
|
||||
int newDir = dir > 0f ? 1 : -1;
|
||||
if (newDir == _facingDir) return;
|
||||
_facingDir = newDir;
|
||||
if (_spriteRenderer != null)
|
||||
|
||||
if (_enableTurnAnimation && _animancer != null && _animConfig?.Turn != null)
|
||||
{
|
||||
_spriteRenderer.flipX = newDir < 0;
|
||||
// 启动转身协程:动画播完后再实际翻转
|
||||
_pendingFacingDir = newDir;
|
||||
if (_turnCoroutine != null) StopCoroutine(_turnCoroutine);
|
||||
_turnCoroutine = StartCoroutine(TurnCoroutine(newDir));
|
||||
}
|
||||
else
|
||||
{
|
||||
// SpriteRenderer 未绑定时通过 localScale 翻转朝向
|
||||
ApplyFacingFlip(newDir);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>转身动画协程:停止水平移动 → 播放 Turn 动画 → 翻转朝向 → 恢复。</summary>
|
||||
private IEnumerator TurnCoroutine(int newDir)
|
||||
{
|
||||
_isTurning = true;
|
||||
StopHorizontal();
|
||||
|
||||
_animancer.Play(_animConfig.Turn);
|
||||
float elapsed = 0f;
|
||||
float duration = _animConfig.Turn.length;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
ApplyFacingFlip(newDir);
|
||||
_isTurning = false;
|
||||
_turnCoroutine = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即中止进行中的转身协程,并将朝向应用到待转方向。
|
||||
/// 受击、死亡、NavLink 穿越等外部中断时调用。
|
||||
/// </summary>
|
||||
public void CancelTurn()
|
||||
{
|
||||
if (_turnCoroutine == null) return;
|
||||
StopCoroutine(_turnCoroutine);
|
||||
_turnCoroutine = null;
|
||||
if (_isTurning)
|
||||
{
|
||||
ApplyFacingFlip(_pendingFacingDir);
|
||||
_isTurning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>真正执行朝向翻转(修改 SpriteRenderer.flipX 或 localScale)。</summary>
|
||||
private void ApplyFacingFlip(int newDir)
|
||||
{
|
||||
_facingDir = newDir;
|
||||
if (_spriteRenderer != null)
|
||||
_spriteRenderer.flipX = newDir < 0;
|
||||
else
|
||||
{
|
||||
Vector3 s = transform.localScale;
|
||||
transform.localScale = new Vector3(Mathf.Abs(s.x) * newDir, s.y, s.z);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
@@ -177,8 +177,8 @@ namespace BaseGames.World.Map
|
||||
if (r != null && !string.IsNullOrEmpty(r.RoomId))
|
||||
_index.TryAdd(r.RoomId, r);
|
||||
}
|
||||
_index.TryGetValue(roomId, out var r);
|
||||
return r;
|
||||
_index.TryGetValue(roomId, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user