根据提供的code differences信息,由于没有具体的代码变更内容,我将生成一个通用的commit message模板:

```
docs(changelog): 更新项目变更日志

- 添加新的版本记录
- 修正已知问题描述
- 更新功能特性说明
```

注意:由于您提供的code differences部分为空,以上是一个示例模板。实际使用时请根据具体的代码变更内容来填写相应的type、scope、subject和body信息。
This commit is contained in:
huadexuan committed 2026-09-17 14:03:34 +08:00
1 parent 69f33d8637
commit 951ba49c79
26 files changed
+942 -455

No files matched your search

+93 -7
View File
@@ -43,17 +43,17 @@ PrefabInstance:
- target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
propertyPath: m_LocalPosition.x
value: -0.8
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
propertyPath: m_LocalPosition.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
propertyPath: m_LocalPosition.y
value: 0.05
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
propertyPath: m_LocalPosition.z
value: 0.9
value: -20
objectReference: {fileID: 0}
- target: {fileID: -8679921383154817045, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
@@ -98,7 +98,7 @@ PrefabInstance:
- target: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
propertyPath: m_TagString
value: Player
value: Friend
objectReference: {fileID: 0}
m_RemovedComponents: []
m_RemovedGameObjects: []
@@ -120,6 +120,22 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 6516315820976732023}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
insertIndex: -1
addedObject: {fileID: 6843810197477671806}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
insertIndex: -1
addedObject: {fileID: 6944470424331181836}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
insertIndex: -1
addedObject: {fileID: 6811651768908473498}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
type: 3}
insertIndex: -1
addedObject: {fileID: 3317273023278999503}
m_SourcePrefab: {fileID: 100100000, guid: 6b859b567d8ba4a7a87c03aa4b87c18c, type: 3}
--- !u!1 &106035464483675767 stripped
GameObject:
@@ -212,6 +228,76 @@ MonoBehaviour:
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
--- !u!114 &6843810197477671806
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 106035464483675767}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
--- !u!114 &6944470424331181836
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 106035464483675767}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 096beceeef30a2646a85a4203af3cd27, type: 3}
m_Name:
m_EditorClassIdentifier:
minX: -9
maxX: 9
minZ: -19
maxZ: 6
moveSpeed: 5
stopDistance: 0.2
groundMask:
serializedVersion: 2
m_Bits: 4294967295
--- !u!114 &6811651768908473498
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 106035464483675767}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
--- !u!136 &3317273023278999503
CapsuleCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 106035464483675767}
m_Material: {fileID: 0}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
m_ExcludeLayers:
serializedVersion: 2
m_Bits: 0
m_LayerOverridePriority: 0
m_IsTrigger: 0
m_ProvidesContacts: 0
m_Enabled: 1
serializedVersion: 2
m_Radius: 0.5
m_Height: 2
m_Direction: 1
m_Center: {x: 0, y: 1, z: 0}
--- !u!4 &1444656758206389486 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 6b859b567d8ba4a7a87c03aa4b87c18c,
+19 -2
View File
@@ -148,6 +148,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 2921261421214297749}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 5072af719b9b1401787c6c55f048d8ed,
type: 3}
insertIndex: -1
addedObject: {fileID: 7706731248061608703}
m_SourcePrefab: {fileID: 100100000, guid: 5072af719b9b1401787c6c55f048d8ed, type: 3}
--- !u!1 &7945271050725079514 stripped
GameObject:
@@ -211,6 +215,7 @@ MonoBehaviour:
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 0}
--- !u!114 &2920089167710187078
MonoBehaviour:
@@ -227,7 +232,7 @@ MonoBehaviour:
maxHealth: 220
currentHealth: 220
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.2
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
@@ -238,7 +243,6 @@ MonoBehaviour:
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
damageNumberOutlineWidth: 0.015
--- !u!114 &4111165692813349771
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -290,6 +294,19 @@ NavMeshAgent:
m_BaseOffset: 0
m_WalkableMask: 4294967295
m_ObstacleAvoidanceType: 4
--- !u!114 &7706731248061608703
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7945271050725079514}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
--- !u!4 &8878706989061864259 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 5072af719b9b1401787c6c55f048d8ed,
+19 -2
View File
@@ -143,6 +143,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 196735238731846736}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 7b0dd5b38a9cd144ea135f3743440958,
type: 3}
insertIndex: -1
addedObject: {fileID: 7984530604249028412}
m_SourcePrefab: {fileID: 100100000, guid: 7b0dd5b38a9cd144ea135f3743440958, type: 3}
--- !u!4 &2222026796505693691 stripped
Transform:
@@ -229,6 +233,7 @@ MonoBehaviour:
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 0}
--- !u!114 &7720102017902192686
MonoBehaviour:
@@ -245,7 +250,7 @@ MonoBehaviour:
maxHealth: 150
currentHealth: 150
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.2
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
@@ -256,7 +261,6 @@ MonoBehaviour:
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
damageNumberOutlineWidth: 0.015
--- !u!114 &1449822714228276666
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -294,6 +298,19 @@ NavMeshAgent:
m_BaseOffset: 0
m_WalkableMask: 4294967295
m_ObstacleAvoidanceType: 4
--- !u!114 &7984530604249028412
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5200308054099318121}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
--- !u!1001 &6707986314085581781
PrefabInstance:
m_ObjectHideFlags: 0
+19 -2
View File
@@ -144,6 +144,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 20042258299677061}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 9d4b54ec2c4f7754bb1fd5ec11a29541,
type: 3}
insertIndex: -1
addedObject: {fileID: 7388673629374319643}
m_SourcePrefab: {fileID: 100100000, guid: 9d4b54ec2c4f7754bb1fd5ec11a29541, type: 3}
--- !u!1 &7062617501994129594 stripped
GameObject:
@@ -223,6 +227,7 @@ MonoBehaviour:
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 0}
--- !u!114 &990449840913200210
MonoBehaviour:
@@ -239,7 +244,7 @@ MonoBehaviour:
maxHealth: 135
currentHealth: 135
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.2
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
@@ -250,7 +255,6 @@ MonoBehaviour:
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
damageNumberOutlineWidth: 0.015
--- !u!114 &3887798621029042906
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -287,6 +291,19 @@ NavMeshAgent:
m_BaseOffset: 0
m_WalkableMask: 4294967295
m_ObstacleAvoidanceType: 4
--- !u!114 &7388673629374319643
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7062617501994129594}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
--- !u!4 &8608542904823568931 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 1852576806548013000, guid: 9d4b54ec2c4f7754bb1fd5ec11a29541,
+19 -2
View File
@@ -199,6 +199,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 4069710806908923055}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 17f7a1b7ae92f0948a5bebde8a11fc2b,
type: 3}
insertIndex: -1
addedObject: {fileID: 4719436641987099519}
m_SourcePrefab: {fileID: 100100000, guid: 17f7a1b7ae92f0948a5bebde8a11fc2b, type: 3}
--- !u!4 &309400887928525066 stripped
Transform:
@@ -283,6 +287,7 @@ MonoBehaviour:
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 0}
--- !u!114 &6884400399417614731
MonoBehaviour:
@@ -299,7 +304,7 @@ MonoBehaviour:
maxHealth: 480
currentHealth: 480
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.35
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
@@ -310,7 +315,6 @@ MonoBehaviour:
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
damageNumberOutlineWidth: 0.015
--- !u!114 &8203338886952913679
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -350,6 +354,19 @@ NavMeshAgent:
m_BaseOffset: 0
m_WalkableMask: 4294967295
m_ObstacleAvoidanceType: 4
--- !u!114 &4719436641987099519
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1242979668690031507}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
--- !u!4 &5170975053921319681 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 6502182839619065283, guid: 17f7a1b7ae92f0948a5bebde8a11fc2b,
+19 -2
View File
@@ -185,6 +185,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 8773165921290396666}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 62d9f853c067ff443bd2a3aa382ab563,
type: 3}
insertIndex: -1
addedObject: {fileID: 897006334484502727}
m_SourcePrefab: {fileID: 100100000, guid: 62d9f853c067ff443bd2a3aa382ab563, type: 3}
--- !u!4 &2585761489654059889 stripped
Transform:
@@ -270,6 +274,7 @@ MonoBehaviour:
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 0}
--- !u!114 &3773753810590757696
MonoBehaviour:
@@ -286,7 +291,7 @@ MonoBehaviour:
maxHealth: 650
currentHealth: 650
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.2
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
@@ -297,7 +302,6 @@ MonoBehaviour:
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
damageNumberOutlineWidth: 0.015
--- !u!114 &4116279677098275843
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -335,3 +339,16 @@ NavMeshAgent:
m_BaseOffset: 0
m_WalkableMask: 4294967295
m_ObstacleAvoidanceType: 4
--- !u!114 &897006334484502727
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3933539425749946856}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
+19 -2
View File
@@ -251,6 +251,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 7158251723525050232}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 2ac38f68ec4f85341b423b5e8defd47e,
type: 3}
insertIndex: -1
addedObject: {fileID: 6065625683664793215}
m_SourcePrefab: {fileID: 100100000, guid: 2ac38f68ec4f85341b423b5e8defd47e, type: 3}
--- !u!1 &4059575966999087028 stripped
GameObject:
@@ -299,7 +303,7 @@ MonoBehaviour:
maxHealth: 100
currentHealth: 100
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.2
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
@@ -310,7 +314,6 @@ MonoBehaviour:
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
damageNumberOutlineWidth: 0.015
--- !u!114 &6486457417548983634
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -342,6 +345,7 @@ MonoBehaviour:
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 0}
--- !u!114 &4858463350953901710
MonoBehaviour:
@@ -394,6 +398,19 @@ NavMeshAgent:
m_BaseOffset: 0
m_WalkableMask: 4294967295
m_ObstacleAvoidanceType: 4
--- !u!114 &6065625683664793215
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4059575966999087028}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
--- !u!4 &7974625701785382694 stripped
Transform:
m_CorrespondingSourceObject: {fileID: 6502182839619065283, guid: 2ac38f68ec4f85341b423b5e8defd47e,
+27 -1
View File
@@ -103,6 +103,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 8144035540121443354}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: 3d031ad534e512e45bb7fe10237e6286,
type: 3}
insertIndex: -1
addedObject: {fileID: 8968669349745731875}
m_SourcePrefab: {fileID: 100100000, guid: 3d031ad534e512e45bb7fe10237e6286, type: 3}
--- !u!1 &6708744566382593167 stripped
GameObject:
@@ -163,6 +167,10 @@ MonoBehaviour:
projectileSpeed: 20
projectileLifetime: 5
energyPerAttack: 20
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 0}
--- !u!114 &6730575396005638825
MonoBehaviour:
@@ -179,12 +187,17 @@ MonoBehaviour:
maxHealth: 360
currentHealth: 360
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.2
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
deathEffectDuration: 1.1
deathEffectColor: {r: 0.55, g: 0.9, b: 1, a: 1}
showDamageNumbers: 1
damageNumberDuration: 0.8
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
--- !u!114 &2583987946621363416
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -214,3 +227,16 @@ MonoBehaviour:
showStunIndicator: 1
indicatorHeight: 0.35
stunIndicatorPrefab: {fileID: 0}
--- !u!114 &8968669349745731875
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6708744566382593167}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
+27 -1
View File
@@ -113,6 +113,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 8367410358801711549}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: d40daebbc0e47416099e97edbbbeac04,
type: 3}
insertIndex: -1
addedObject: {fileID: 507015780925844398}
m_SourcePrefab: {fileID: 100100000, guid: d40daebbc0e47416099e97edbbbeac04, type: 3}
--- !u!1 &4471054466352856454 stripped
GameObject:
@@ -147,6 +151,10 @@ MonoBehaviour:
projectileSpeed: 20
projectileLifetime: 5
energyPerAttack: 20
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 0}
--- !u!143 &7319997191678055287
CharacterController:
@@ -189,12 +197,17 @@ MonoBehaviour:
maxHealth: 820
currentHealth: 820
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.5
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
deathEffectDuration: 1.1
deathEffectColor: {r: 0.55, g: 0.9, b: 1, a: 1}
showDamageNumbers: 1
damageNumberDuration: 0.8
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
--- !u!114 &1445493180626066942
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -227,3 +240,16 @@ MonoBehaviour:
skillRadius: 5
stunDuration: 1
skillAnimationDuration: 0
--- !u!114 &507015780925844398
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4471054466352856454}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
+27 -1
View File
@@ -128,6 +128,10 @@ PrefabInstance:
type: 3}
insertIndex: -1
addedObject: {fileID: 1038858676101729388}
- targetCorrespondingSourceObject: {fileID: 919132149155446097, guid: aedf6e849dd924e229c57640f99ff50a,
type: 3}
insertIndex: -1
addedObject: {fileID: 761483117139778608}
m_SourcePrefab: {fileID: 100100000, guid: aedf6e849dd924e229c57640f99ff50a, type: 3}
--- !u!1 &1823520504047441011 stripped
GameObject:
@@ -188,6 +192,10 @@ MonoBehaviour:
projectileSpeed: 20
projectileLifetime: 5
energyPerAttack: 20
separationRadius: 1.5
separationWeight: 3
targetSpreadBias: 6
summonerMode: 0
target: {fileID: 1823520504047441011}
--- !u!114 &2631642551191811442
MonoBehaviour:
@@ -204,12 +212,17 @@ MonoBehaviour:
maxHealth: 400
currentHealth: 400
maxEnergy: 100
currentEnergy: 0
_currentEnergy: 0
heightOffset: 0.25
barWidth: 1.2
borderColor: {r: 0.03, g: 0.03, b: 0.03, a: 0.95}
deathEffectDuration: 1.1
deathEffectColor: {r: 0.1, g: 0.55, b: 1, a: 1}
showDamageNumbers: 1
damageNumberDuration: 0.8
damageNumberRiseHeight: 1.2
damageNumberSpawnHeight: 1.5
damageNumberSize: 0.09
--- !u!114 &8856053484402202215
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -240,3 +253,16 @@ MonoBehaviour:
indicatorHeight: 0.35
stunIndicatorPrefab: {fileID: 2432573200162841629, guid: de288ea16be0c1544a4c6eb71e899570,
type: 3}
--- !u!114 &761483117139778608
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1823520504047441011}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e6f40b66b50871440a97763c44d480a4, type: 3}
m_Name:
m_EditorClassIdentifier:
teamId: 0
Binary file not shown.
+141
View File
@@ -0,0 +1,141 @@
using UnityEngine;
/// <summary>
/// AI 召唤师控制器:自动召唤单位攻击玩家
/// </summary>
public class AISummoner : MonoBehaviour
{
[Header("AI 配置")]
[Tooltip("召唤间隔(秒)")]
[Min(1f)]
public float summonInterval = 3f;
[Tooltip("随机偏移范围(秒),避免太规律")]
[Min(0f)]
public float summonRandomOffset = 1f;
[Tooltip("AI 难度:召唤CD缩放(越小越快)")]
[Range(0.5f, 2f)]
public float difficultyScale = 1f;
[Header("能量管理")]
[Tooltip("AI 初始能量")]
[Min(0)]
public int startingMana = 150;
[Tooltip("AI 是否使用无限能量")]
public bool unlimitedMana = false;
private MainCharacterCombat combat;
private ManaSystem manaSystem;
private float summonCooldown;
private bool isActive;
private void Start()
{
combat = GetComponent<MainCharacterCombat>();
if (combat == null)
{
Debug.LogError($"[AISummoner] {name} 缺少 MainCharacterCombat 组件!");
enabled = false;
return;
}
manaSystem = FindObjectOfType<ManaSystem>();
if (manaSystem == null)
{
Debug.LogWarning($"[AISummoner] 场景中没有 ManaSystem,AI 将无法检查能量");
}
// 初始延迟,避免开局立刻召唤
summonCooldown = summonInterval * difficultyScale;
isActive = true;
Debug.Log($"[AISummoner] {name} AI 初始化完成,召唤间隔: {summonInterval}s");
}
private void Update()
{
if (!isActive) return;
// AI 被眩晕时停止召唤
if (combat.IsStunned) return;
summonCooldown -= Time.deltaTime;
if (summonCooldown <= 0f)
{
TrySummon();
ResetCooldown();
}
}
private void TrySummon()
{
// 随机选择一个可召唤的角色槽位(1-5)
var characters = combat.summonableCharacters;
if (characters == null || characters.Length == 0)
{
Debug.LogWarning($"[AISummoner] {name} 没有可召唤的角色");
return;
}
// 过滤出有效的角色数据
int[] validSlots = new int[characters.Length];
int validCount = 0;
for (int i = 0; i < characters.Length; i++)
{
if (characters[i] != null && characters[i].unitPrefab != null)
{
// 检查能量
if (unlimitedMana || manaSystem == null || manaSystem.CurrentManaInt >= characters[i].manaCost)
{
validSlots[validCount++] = i + 1; // slot 是 1-based
}
}
}
if (validCount == 0)
{
// 没有可召唤的角色(能量不足或没有有效角色)
return;
}
// 随机选择一个有效槽位
int randomIndex = Random.Range(0, validCount);
int selectedSlot = validSlots[randomIndex];
// 执行召唤(使用 MainCharacterCombat.RequestSummon)
bool success = combat.RequestSummon(selectedSlot);
if (success)
{
var selectedCharacter = characters[selectedSlot - 1];
Debug.Log($"[AISummoner] {name} 召唤了 {selectedCharacter.characterName} (槽位: {selectedSlot})");
}
}
private void ResetCooldown()
{
float randomOffset = Random.Range(-summonRandomOffset, summonRandomOffset);
summonCooldown = summonInterval * difficultyScale + randomOffset;
}
/// <summary>
/// 启用/禁用 AI
/// </summary>
public void SetActive(bool active)
{
isActive = active;
Debug.Log($"[AISummoner] {name} AI {(active ? "启用" : "禁用")}");
}
/// <summary>
/// 调整难度(实时修改召唤速度)
/// </summary>
public void SetDifficulty(float scale)
{
difficultyScale = Mathf.Clamp(scale, 0.5f, 2f);
Debug.Log($"[AISummoner] {name} 难度调整为 {difficultyScale}");
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CCke4S2lUy3nU786T7yvHZYNAkerlhq9/n+qIWEgtRjk/6EPe8RNOPo=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+13
View File
@@ -88,6 +88,19 @@ public class Barracks : MonoBehaviour
unit.name = characterData.characterName;
unit.tag = unitTag;
// 设置阵营 ID(根据建筑的阵营)
var buildingTeam = GetComponent<UnitTeam>();
var unitTeam = unit.GetComponent<UnitTeam>();
if (buildingTeam != null && unitTeam != null)
{
unitTeam.teamId = buildingTeam.teamId;
}
else if (unitTeam != null)
{
// 兜底:根据 tag 判断阵营
unitTeam.teamId = unitTag == "Friend" ? 0 : 1;
}
// 如果单位有UnitBase组件,使用1级属性初始化
var unitBase = unit.GetComponent<UnitBase>();
if (unitBase != null)
+7
View File
@@ -235,6 +235,13 @@ public class BattleSetupManager : MonoBehaviour
unit.name = "Friend_" + data.characterName;
unit.tag = "Friend";
// 设置阵营 ID(玩家单位为 team 0)
var unitTeam = unit.GetComponent<UnitTeam>();
if (unitTeam != null)
{
unitTeam.teamId = 0;
}
var unitBase = unit.GetComponent<UnitBase>();
if (unitBase != null)
{
+37
View File
@@ -0,0 +1,37 @@
using UnityEngine;
/// <summary>
/// 游戏管理器:管理全局游戏状态,包括本地玩家的阵营 ID。
/// PVE:LocalPlayerTeamId = 0(固定)
/// PVP:服务器分配后设置 LocalPlayerTeamId(比如 0 或 1)
/// </summary>
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("本地玩家阵营")]
[Tooltip("PVE: 固定为0; PVP: 服务器分配")]
public int LocalPlayerTeamId = 0;
private void Awake()
{
// 单例模式
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
/// <summary>
/// 设置本地玩家阵营 ID(PVP 时由服务器调用)
/// </summary>
public void SetLocalPlayerTeam(int teamId)
{
LocalPlayerTeamId = teamId;
Debug.Log($"本地玩家阵营设置为: {teamId}");
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: CXkdt3ulUS3TPoI5TieIx0QPMa6EXoIKUewqD1fNwBgwo43HfDPsYzY=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+89 -7
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(StatusEffectController))]
public class MainCharacterCombat : MonoBehaviour
{
[Header("自动攻击")]
@@ -42,6 +43,7 @@ public class MainCharacterCombat : MonoBehaviour
[Min(0f)]
public float projectileLifetime = 5f;
private StatusEffectController statusEffectController;
private Animator animator;
private MainCharacterDebugMovement movement;
private GameObject target;
@@ -56,10 +58,18 @@ public class MainCharacterCombat : MonoBehaviour
private bool summonSpawned;
private bool summoning;
public bool IsStunned => statusEffectController != null && statusEffectController.IsStunned;
private void Start()
{
animator = GetComponent<Animator>();
movement = GetComponent<MainCharacterDebugMovement>();
statusEffectController = GetComponent<StatusEffectController>();
if (statusEffectController == null)
{
statusEffectController = gameObject.AddComponent<StatusEffectController>();
}
if (animator != null)
{
@@ -69,6 +79,16 @@ public class MainCharacterCombat : MonoBehaviour
private void Update()
{
// 眩晕时停止所有行动
if (IsStunned)
{
if (animator != null)
{
animator.SetInteger("speed", 0);
}
return;
}
if (attackCooldown > 0f)
{
attackCooldown -= Time.deltaTime;
@@ -230,11 +250,12 @@ public class MainCharacterCombat : MonoBehaviour
return;
}
// 立即停止移动
// 立即停止移动,并禁用移动脚本(防止干扰动画)
SummonerClickMovement clickMovement = GetComponent<SummonerClickMovement>();
if (clickMovement != null)
{
clickMovement.ClearTarget();
clickMovement.enabled = false; // 禁用移动脚本,避免它设置 speed 参数
}
pendingSummonPrefab = prefab;
@@ -244,9 +265,17 @@ public class MainCharacterCombat : MonoBehaviour
summonSpawned = false;
summoning = true;
animator.speed = 1f;
// 强制重置动画状态,避免状态过渡冲突
animator.SetInteger("speed", 0);
animator.ResetTrigger("skill");
animator.ResetTrigger("attack");
// 直接强制播放 skill 动画,绕过状态机的所有过渡条件
// Play() 会立即切换,不管当前状态是什么
animator.Play("skill", 0, 0f);
Debug.Log("强制播放 skill 动画");
}
public void OnSkillHit1()
@@ -272,7 +301,22 @@ public class MainCharacterCombat : MonoBehaviour
pendingSummonPrefab,
spawnPosition,
Quaternion.LookRotation(direction, Vector3.up));
summoned.tag = "Friend";
// 召唤的单位继承召唤师的阵营(不再硬编码为 Friend)
UnitTeam summonerTeam = GetComponent<UnitTeam>();
UnitTeam summonedTeam = summoned.GetComponent<UnitTeam>();
if (summonerTeam != null && summonedTeam != null)
{
summonedTeam.teamId = summonerTeam.teamId;
// 根据阵营设置 tag(向后兼容旧系统)
summoned.tag = summonerTeam.teamId == 0 ? "Friend" : "Enemy";
}
else
{
// 降级:没有 UnitTeam 时保持原逻辑
summoned.tag = "Friend";
}
UnitBase unitBase = summoned.GetComponent<UnitBase>();
if (unitBase != null && pendingSummonCharacterData != null && pendingSummonPlayerData != null)
@@ -404,16 +448,25 @@ public class MainCharacterCombat : MonoBehaviour
private void CheckSummonFinished()
{
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0);
// 正常完成:skill 动画播放完毕
if (state.IsName("skill") && state.normalizedTime >= 1f)
{
FinishSummon();
return;
}
// skill 状态意外被其他动画打断时,才使用超时兜底,避免永久锁定召唤。
// 超时检测:添加调试信息
if (summonElapsed >= summonAnimationTimeout)
{
Debug.LogWarning("主角召唤动画超时,已解除召唤状态");
// 输出当前动画状态,帮助诊断问题
string currentState = "unknown";
if (state.IsName("skill")) currentState = "skill";
else if (state.IsName("idle")) currentState = "idle";
else if (state.IsName("run")) currentState = "run";
else if (state.IsName("attack")) currentState = "attack";
Debug.LogWarning($"主角召唤动画超时,已解除召唤状态。当前动画状态: {currentState}, normalizedTime: {state.normalizedTime:F2}");
FinishSummon();
}
}
@@ -427,6 +480,15 @@ public class MainCharacterCombat : MonoBehaviour
animator.speed = 1f;
animator.SetInteger("speed", 0);
animator.Play("idle", 0, 0f);
// 重新启用移动脚本
SummonerClickMovement clickMovement = GetComponent<SummonerClickMovement>();
if (clickMovement != null)
{
clickMovement.enabled = true;
}
Debug.Log("召唤完成,恢复移动脚本");
}
private void CheckAttackFinished()
@@ -535,18 +597,38 @@ public class MainCharacterCombat : MonoBehaviour
private GameObject FindNearestEnemy()
{
// 索敌范围使用较小的 detectRange,主角不主动远离基地寻找敌人
UnitTeam myTeam = GetComponent<UnitTeam>();
if (myTeam == null)
{
return null;
}
GameObject nearest = null;
float nearestDistance = detectRange * detectRange;
foreach (GameObject enemy in GameObject.FindGameObjectsWithTag("Enemy"))
// 查找所有带 UnitTeam 的对象(包括单位和召唤师)
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
foreach (UnitTeam targetTeam in allTeams)
{
if (enemy == null || enemy.transform.root == transform.root)
GameObject enemy = targetTeam.gameObject;
if (enemy == null || enemy == gameObject || enemy.transform.root == transform.root)
{
continue;
}
// 检查是否是敌人(使用阵营 ID)
if (!myTeam.IsEnemy(enemy))
{
continue;
}
// 检查是否有 UnitBase 或 WorldHealthBar(召唤师)
UnitBase enemyUnit = enemy.GetComponentInParent<UnitBase>();
if (enemyUnit == null)
WorldHealthBar enemyHealth = enemy.GetComponentInParent<WorldHealthBar>();
if (enemyUnit == null && enemyHealth == null)
{
continue;
}
+11 -2
View File
@@ -116,20 +116,29 @@ public class Projectile : MonoBehaviour
UnitBase targetUnit = hitObject.GetComponentInParent<UnitBase>();
BuildingHealth targetBuilding = hitObject.GetComponentInParent<BuildingHealth>();
if (targetUnit == null && targetBuilding == null)
WorldHealthBar targetHealthBar = hitObject.GetComponentInParent<WorldHealthBar>();
if (targetUnit == null && targetBuilding == null && targetHealthBar == null)
{
return;
}
hasHit = true;
if (targetUnit != null)
{
targetUnit.ReceiveDamage(damage, owner);
}
else
else if (targetBuilding != null)
{
targetBuilding.ReceiveDamage(damage, owner);
}
else if (targetHealthBar != null)
{
// 召唤师等特殊单位(有 WorldHealthBar 但没有 UnitBase)
targetHealthBar.TakeDamage(damage, owner);
}
if (registerEnergyOnHit && owner != null)
{
UnitBase unit = owner.GetComponent<UnitBase>();
+89 -35
View File
@@ -342,48 +342,50 @@ public class UnitBase : MonoBehaviour
//根据自身Tag找到敌对Tag
private string GetOpponentTag()
{
if (CompareTag("Friend"))
{
return "Enemy";
}
if (CompareTag("Enemy"))
{
return "Friend";
}
// 已弃用:改用阵营 ID 系统,不再依赖 Tag 判断敌我
// 保留此方法是为了兼容旧代码,但不应再被调用
Debug.LogWarning("GetOpponentTag() 已弃用,请使用阵营 ID 系统");
return null;
}
//查找距离自己最近的敌对目标
//查找距离自己最近的敌对目标(使用阵营 ID 判断)
private GameObject FindNearestOpponent()
{
string opponentTag = GetOpponentTag();
if (string.IsNullOrEmpty(opponentTag))
var myTeam = GetComponent<UnitTeam>();
if (myTeam == null)
{
Debug.LogWarning($"{gameObject.name} 缺少 UnitTeam 组件,无法查找敌人");
return null;
}
GameObject nearestOpponent = null;
float nearestSqrDistance = float.PositiveInfinity;
GameObject[] opponents = GameObject.FindGameObjectsWithTag(opponentTag);
foreach (var opponent in opponents)
// 查找所有 UnitTeam 组件(不再依赖 Tag)
UnitTeam[] allUnits = FindObjectsOfType<UnitTeam>();
foreach (var unit in allUnits)
{
//排除自己以及自己所在层级中的对象
if (opponent == gameObject || opponent.transform.root == transform.root)
// 跳过自己和友军
if (unit.gameObject == gameObject || !myTeam.IsEnemy(unit.gameObject))
{
continue;
}
float sqrDistance = (opponent.transform.position - transform.position).sqrMagnitude;
//排除自己所在层级中的对象
if (unit.transform.root == transform.root)
{
continue;
}
float sqrDistance = (unit.transform.position - transform.position).sqrMagnitude;
//加入确定性分散偏移:让不同单位偏好不同目标,避免全部涌向同一个敌人
float bias = ((uint)(gameObject.GetInstanceID() * 7919) ^ (uint)(opponent.GetInstanceID())) % 10u / 10f * targetSpreadBias;
float bias = ((uint)(gameObject.GetInstanceID() * 7919) ^ (uint)(unit.gameObject.GetInstanceID())) % 10u / 10f * targetSpreadBias;
float adjustedSqrDistance = sqrDistance + bias;
if (adjustedSqrDistance < nearestSqrDistance)
{
nearestSqrDistance = adjustedSqrDistance;
nearestOpponent = opponent;
nearestOpponent = unit.gameObject;
}
}
@@ -651,6 +653,15 @@ public class UnitBase : MonoBehaviour
if (targetBuilding != null && targetBuilding.ReceiveDamage(attackPower, gameObject))
{
RegisterAttackHit();
return;
}
// 召唤师等特殊单位(有 WorldHealthBar 但没有 UnitBase)
WorldHealthBar targetHealthBar = opponent.GetComponentInParent<WorldHealthBar>();
if (targetHealthBar != null)
{
targetHealthBar.TakeDamage(attackPower, gameObject);
RegisterAttackHit();
}
}
@@ -689,23 +700,33 @@ public class UnitBase : MonoBehaviour
return;
}
string opponentTag = GetOpponentTag();
if (string.IsNullOrEmpty(opponentTag))
UnitTeam myTeam = GetComponent<UnitTeam>();
if (myTeam == null)
{
return;
}
float clampedRadius = Mathf.Max(0f, radius);
float radiusSquared = clampedRadius * clampedRadius;
GameObject[] opponents = GameObject.FindGameObjectsWithTag(opponentTag);
// 查找所有带 UnitTeam 的对象(包括单位和召唤师)
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
HashSet<WorldHealthBar> hitHealthBars = new HashSet<WorldHealthBar>();
foreach (GameObject opponent in opponents)
foreach (UnitTeam targetTeam in allTeams)
{
GameObject opponent = targetTeam.gameObject;
if (opponent == null || opponent == gameObject || opponent.transform.root == transform.root)
{
continue;
}
// 检查是否是敌人
if (!myTeam.IsEnemy(opponent))
{
continue;
}
Vector3 offset = opponent.transform.position - transform.position;
offset.y = 0f;
@@ -716,9 +737,21 @@ public class UnitBase : MonoBehaviour
UnitBase targetUnit = opponent.GetComponentInParent<UnitBase>();
BuildingHealth targetBuilding = opponent.GetComponentInParent<BuildingHealth>();
WorldHealthBar healthBar = targetUnit != null
? targetUnit.GetComponent<WorldHealthBar>()
: targetBuilding != null ? targetBuilding.GetComponent<WorldHealthBar>() : null;
WorldHealthBar healthBar = null;
if (targetUnit != null)
{
healthBar = targetUnit.GetComponent<WorldHealthBar>();
}
else if (targetBuilding != null)
{
healthBar = targetBuilding.GetComponent<WorldHealthBar>();
}
else
{
// 召唤师等特殊单位(有 WorldHealthBar 但没有 UnitBase)
healthBar = opponent.GetComponentInParent<WorldHealthBar>();
}
if (healthBar == null || !hitHealthBars.Add(healthBar))
{
@@ -729,10 +762,15 @@ public class UnitBase : MonoBehaviour
{
targetUnit.ReceiveDamage(damage, gameObject);
}
else
else if (targetBuilding != null)
{
targetBuilding.ReceiveDamage(damage, gameObject);
}
else if (healthBar != null)
{
// 召唤师等特殊单位
healthBar.TakeDamage(damage, gameObject);
}
}
if (registerEnergy)
@@ -748,23 +786,33 @@ public class UnitBase : MonoBehaviour
return;
}
string opponentTag = GetOpponentTag();
if (string.IsNullOrEmpty(opponentTag))
UnitTeam myTeam = GetComponent<UnitTeam>();
if (myTeam == null)
{
return;
}
float clampedRadius = Mathf.Max(0f, radius);
float radiusSquared = clampedRadius * clampedRadius;
GameObject[] opponents = GameObject.FindGameObjectsWithTag(opponentTag);
// 查找所有带 UnitTeam 的对象(包括单位和召唤师)
UnitTeam[] allTeams = FindObjectsOfType<UnitTeam>();
HashSet<UnitBase> hitUnits = new HashSet<UnitBase>();
foreach (GameObject opponent in opponents)
foreach (UnitTeam targetTeam in allTeams)
{
GameObject opponent = targetTeam.gameObject;
if (opponent == null || opponent == gameObject || opponent.transform.root == transform.root)
{
continue;
}
// 检查是否是敌人
if (!myTeam.IsEnemy(opponent))
{
continue;
}
Vector3 offset = opponent.transform.position - transform.position;
offset.y = 0f;
@@ -774,12 +822,18 @@ public class UnitBase : MonoBehaviour
}
UnitBase unit = opponent.GetComponentInParent<UnitBase>();
if (unit == null || !hitUnits.Add(unit))
if (unit != null && hitUnits.Add(unit))
{
unit.ApplyStun(duration);
continue;
}
unit.ApplyStun(duration);
// 召唤师等特殊单位(有 StatusEffectController 但没有 UnitBase)
StatusEffectController statusEffect = opponent.GetComponentInParent<StatusEffectController>();
if (statusEffect != null)
{
statusEffect.ApplyStun(duration);
}
}
}
+47
View File
@@ -0,0 +1,47 @@
using UnityEngine;
/// <summary>
/// 单位阵营标记:用阵营 ID 替代 Unity Tag 判断敌我关系。
/// 支持 PVP:客户端只需设置 GameManager.LocalPlayerTeamId,所有单位的敌我判断自动正确。
/// </summary>
public class UnitTeam : MonoBehaviour
{
[Header("阵营 ID")]
[Tooltip("0 = 玩家1阵营,1 = 玩家2阵营,2+ = 其他阵营")]
public int teamId = 0;
/// <summary>
/// 判断目标是否为敌人(不同阵营)
/// </summary>
public bool IsEnemy(GameObject other)
{
if (other == null) return false;
var otherTeam = other.GetComponent<UnitTeam>();
if (otherTeam == null) return false;
return otherTeam.teamId != this.teamId;
}
/// <summary>
/// 判断目标是否为友军(相同阵营)
/// </summary>
public bool IsFriendly(GameObject other)
{
if (other == null) return false;
var otherTeam = other.GetComponent<UnitTeam>();
if (otherTeam == null) return false;
return otherTeam.teamId == this.teamId;
}
/// <summary>
/// 判断是否属于本地玩家阵营
/// </summary>
public bool IsLocalPlayerTeam()
{
if (GameManager.Instance == null) return teamId == 0;
return teamId == GameManager.Instance.LocalPlayerTeamId;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: WnxJsSr/VX1DeBY80HGM2j/amZWAkx8FQCM7bFTomhdecHzVdHd5eOw=
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+10 -3
View File
@@ -484,9 +484,16 @@ public class WorldHealthBar : MonoBehaviour
private Color GetHealthFillColor()
{
return CompareTag("Enemy")
? new Color(0.88f, 0.12f, 0.12f, 1f)
: new Color(0.1f, 0.8f, 0.18f, 1f);
// 使用阵营 ID 判断敌我(支持 PVP)
var unitTeam = GetComponent<UnitTeam>();
if (unitTeam != null && unitTeam.IsLocalPlayerTeam())
{
return new Color(0.1f, 0.8f, 0.18f, 1f); // 绿色(我方)
}
else
{
return new Color(0.88f, 0.12f, 0.12f, 1f); // 红色(敌方)
}
}
private Color GetEnergyFillColor()
+6
View File
@@ -23,6 +23,7 @@
- [2026-09-14 18:24:56] 用户认可"参考游戏是基地离得远、镜头更远、单位推进慢"的观感方向(2026-09-14 数值改慢后反馈"好多了")。**Why:** 之前双方距离近+移速快,单位两三秒就接触,战斗缺乏观察窗口。**How to apply:** 后续地图/镜头调整优先加长双方距离、拉远摄像机;数值宁慢勿快。
- [2026-09-16 22:18:14] 召唤系统:找不到合适位置时直接在召唤师前方强制生成,不显示警告,不退还能量。**Why:** 用户要求召唤必须成功,不被取消。**How to apply:** MainCharacterCombat.cs 的召唤逻辑
- [2026-09-16 22:18:30] 召唤打断移动:主角点击召唤时必须立即停止移动,调用 SummonerClickMovement.ClearTarget() 来实现。**Why:** 用户要求召唤时即使在移动也要立刻停下。**How to apply:** BeginSummon 开始时停止移动
- [2026-09-17 10:06:34] 召唤师移动后连续召唤动画超时问题:SummonerClickMovement.HandleMovement() 每帧设置 animator.SetInteger("speed", 1) 干扰召唤动画。**Why:** 即使召唤时调用了 ClearTarget(),下一帧 Update 还是会覆盖动画参数。**How to apply:** 召唤期间禁用移动脚本 (clickMovement.enabled = false),召唤完成后重新启用 (enabled = true)。已在 MainCharacterCombat.Summon() 和 FinishSummon() 中实现。
### Project
- [2026-08-28 14:26:56] 弓箭手远程箭矢的设计要求:保持发射时高度水平飞行,命中目标后直接结算伤害并销毁,不产生实体物理击退。**Why:** 当前玩法希望远程攻击是命中判定而非物理推动。**How to apply:** 后续调整箭矢、Projectile 或弓箭手攻击逻辑时保持这一行为。
@@ -57,6 +58,11 @@
- [2026-09-15 19:33:31] 金币系统已完成:GoldManager 单例管理,初始500金币,右上角UI实时更新。金矿建筑产出50金币/5秒,主角产出30金币/8秒。WorldHealthBar 的 currentEnergy 改为属性(带 setter 自动调用 UpdateEnergyFill),能量条尺寸从8像素增加到12像素(EnergyPixelHeight=12,EnergyPixelWidth=100)。能量条显示金币产出进度,满了产出并重置。已更新核心玩法文档。
- [2026-09-16 14:22:13] 2026-09-16 玩法定稿为"召唤师对决"并已写入《召唤之战-核心玩法.md》:无基地/金矿/兵营/防御塔,玩家手动点地操控召唤师(移动不攻击、站定攻击),限己方半场(三分屏:己方/交战/敌方区),能量直购出兵(单位在召唤师身边生成),击杀敌方召唤师获胜,超时毒圈收缩并解除半场限制,中期混沌裂缝事件,召唤师技能 3~4 种,摇杆留作实测后升级项。**Why:** 用户多轮迭代后的最终方向,旧"建筑生产+自动作战"对攻玩法封存不删。**How to apply:** 新战斗开发一律基于召唤师对决方案;不要在新玩法场景引入金矿/兵营/建筑摆放;BuildingPlacementManager/Barracks/GoldMine 等封存留作后续建筑卡或 PVE 特殊关素材。
- [2026-09-16 14:48:52] 召唤师对决首版场景已完成(SummonerDuelTest.unity,2026-09-16):新场景地面20×80、固定正交相机43°/orthoSize28、召唤师z=0(敌方z=35)均避开底部UI;新增 SummonerClickMovement(点地走位+clamp X[-9,9] Z[-19,6],移动中关攻击、到点恢复)、SummonerDuelManager(点卡片按能量直召,单位在召唤师身边生成)、UnitBase 新增 summonerMode 开关(不追击、站定攻击,默认关闭不影响旧单位);战斗界面UI从 CoreBattleTest 整树搬运复用,SummonCardButton 增加对决模式分支(无布阵/金币管理器时走能量直召+冷却)。主角已存为 Assets/Prefabs/主角/召唤师主角.prefab。**How to apply:** 后续对决玩法迭代(毒圈/胜负判定/召唤师技能)基于该场景与脚本扩展;Play Mode 验证等用户手动执行。
- [2026-09-17 09:33:46] 阵营 ID 系统已实现(替代 Unity Tag 判断敌我):UnitTeam 组件标记 teamId,GameManager.LocalPlayerTeamId 标识本地玩家阵营,支持 PVP(真人 vs 真人 / 真人 vs 机器人)。**Why:** PVP 需要动态敌我判断,Tag 系统无法支持(双方客户端看到的 tag 矛盾)。**How to apply:** 所有敌我判断改用 UnitTeam.IsEnemy(),血条颜色用 IsLocalPlayerTeam(),新生成单位必须设置 teamId(Barracks/BattleSetupManager 已修改)。
- [2026-09-17 09:33:58] 召唤师组件精简:移除了 NavMeshAgent、UnitBase、StatusEffectController,保留 MainCharacterCombat(战斗)+ SummonerClickMovement(移动)+ WorldHealthBar(血条)。SummonerClickMovement 已改为依赖 MainCharacterCombat 而非 UnitBase。**Why:** 召唤师是特殊单位,不应使用通用 UnitBase 逻辑;直线移动不需要 NavMesh。**How to apply:** 召唤师专属逻辑用 MainCharacterCombat,普通单位用 UnitBase。
- [2026-09-17 09:34:13] 《召唤之战》场地规格:60×20(Z×X),三区划分各 20 单位 - 己方区 z∈[-30,-10](召唤师 z=-20),交战区 z∈[-10,10],敌方区 z∈[10,30](敌方召唤师 z=+20)。召唤师移动限制在己方区内(SummonerClickMovement clamp Z[-29,-11])。相机 orthoSize=22,位置(0,40,-40),角度 43°。**Why:** 竖屏竞技场设计,双方对称 40 单位间距,防风筝规则(召唤师不能进入交战区)。**How to apply:** 所有位置/spawn 相关代码使用新的 Z 坐标范围。
- [2026-09-17 10:06:48] Projectile 伤害判定逻辑:HitTarget() 检查 UnitBase(普通单位)、BuildingHealth(建筑)、WorldHealthBar(召唤师等特殊单位)三种受击组件。召唤师使用 MainCharacterCombat + WorldHealthBar,不挂 UnitBase。**Why:** 召唤师是玩家控制的特殊单位,行为逻辑与普通 AI 单位不同。**How to apply:** 新增特殊单位必须有 WorldHealthBar 或 UnitBase 之一才能被攻击;法术球击中后调用对应组件的受伤方法(UnitBase.ReceiveDamage / WorldHealthBar.TakeDamage)。
- [2026-09-17 10:38:49] 召唤师(MainCharacterCombat)与普通单位(UnitBase)的架构差异:召唤师使用 MainCharacterCombat + WorldHealthBar + StatusEffectController,不挂 UnitBase。**Why:** 召唤师是玩家控制的特殊单位,行为逻辑与 AI 单位不同。**How to apply:** 涉及单位的通用系统(伤害、眩晕、范围效果)必须同时支持 UnitBase 和非 UnitBase 路径。已修复:Projectile.HitTarget()、UnitBase.ApplyDirectDamage()、UnitBase.ApplyAreaStun() 均添加了 WorldHealthBar / StatusEffectController 的检测分支。
### Reference
- [2026-09-09 12:23:17] 飞行物脚本 Projectile.cs 负责锁敌追踪、碰撞检测和伤害结算。箭矢预制体 ArrowProjectile_White.prefab 已配置 Projectile 组件和 CapsuleCollider(Is Trigger)。远程单位需在攻击动画松弦帧添加 OnAttackEffect Event,并在 UnitBase 中设置 attackType=RangedProjectile、projectilePrefab、projectileSpawnPoint。
+171
View File
@@ -0,0 +1,171 @@
# 召唤之战-核心玩法拆家版
> 版本:V1.1
> 定位:竖版手机休闲即时战略小游戏
> 单局时长:1~3分钟
> 核心目标:摧毁敌方主基地获胜
> 核心特点:轻量操作、自由寻路、资源与出兵据点争夺、兵种克制,无复杂框选与编队
## 一、核心定位
本玩法为《召唤之战》的「拆家流」经典RTS版本,玩家通过消耗法力召唤单位,争夺战场上的水晶矿与随机出兵点,建立经济或兵力优势,最终突破防御摧毁敌方基地取胜。
- 操作极简:仅需「选择出兵目标 + 点击卡牌召唤单位」,无需框选、编队、驻守指令;
- 单位全自动作战:复用自动寻路与寻敌逻辑,玩家不干预单位微操;
- 策略核心:出兵目标选择、兵种搭配、资源分配节奏、资源点与出兵点取舍,避免纯数值对撞。
## 二、战场地图设计
### 2.1 整体布局
采用**竖版9:16比例**,核心可通行区域为十字形,外围为不可通行障碍物(岩石/树林)。既限制战场范围避免混乱,又保留多路径进攻空间,杜绝单路排队式战斗。
┌──────────────────────────────┐
│ 障碍物(不可通行) │
│ ┌────────────────────────┐ │
│ │ 【敌方城堡】 │ │
│ │ 【敌方防御塔】 │ │
│ │ │ │
│ │ ●─────────● │ │
│ │ 水晶矿 出兵点 │ │
│ │ (横向通道两端) │ │
│ │ │ │
│ │ 【我方防御塔】 │ │
│ │ 【我方城堡】 │ │
│ └────────────────────────┘ │
│ 障碍物(不可通行) │
└──────────────────────────────┘
### 2.2 区域说明
| 区域 | 位置 | 规则 |
|------|------|------|
| 己方阵地 | 屏幕下方 | 包含我方城堡、防御塔,为单位出生点 |
| 敌方阵地 | 屏幕上方 | 包含敌方城堡、防御塔,为最终攻击目标 |
| 十字主通道 | 战场中部 | 纵向连通双方阵地,横向连通水晶矿与出兵点;单位可在区域内自由移动 |
| 水晶矿 | 横向通道一端 | 可攻击争夺目标,占领后持续提升法力回复速度 |
| 随机出兵点 | 横向通道另一端 | 可攻击争夺目标,占领后定时随机产出单位 |
### 2.3 寻路规则
- 基于NavMesh实现自由寻路,仅十字区域为可通行烘焙区,障碍物完全阻挡;
- 单位自动计算最短路径前往目标,途中自动避让友军与地形;
- 无强制固定路径,单位可在十字区域内任意移动,避免排队式战斗。
## 三、资源系统
### 3.1 法力水晶
- 战斗内唯一资源,用于召唤单位、释放技能;
- 基础自动回复,开局默认速度,上限10点;
- 占领水晶矿可提升回复速度,具体回复加成数值以测试为准;
### 3.2 资源据点争夺规则
- 水晶矿与随机出兵点均为可攻击目标,不设置生命值,使用**红蓝争夺进度条**(0~100);
- 进度条中立状态为50:50。以当前观察方为例,100表示该方完全占领,0表示对方完全占领;
- 己方单位攻击据点时,进度条向己方方向偏移;敌方攻击时,进度条向敌方方向偏移;
- 进度条达到己方100%时,据点归属己方并开始产出对应资源;
- 据点归属后,即使敌方开始攻击并推动进度条反向移动,原占有方仍持续获得产出;
- 只有当进度条被对方完全推到100%(即原占有方进度为0)时,才中止原占有方产出并将据点翻转给对方;
- 无任何单位攻击时,进度条保持当前数值不变;
- 据点翻转后,按相同规则由新的占有方获得对应产出。
### 3.3 水晶矿产出
- 水晶矿归属己方后,持续提升己方法力回复速度;
- 水晶矿被对方完全翻转后,法力回复加成立即转移给对方;
- 单个水晶矿的回复加成与多个据点叠加规则,以战斗测试结果为准。
### 3.4 随机出兵点产出
- 随机出兵点归属己方后,按固定间隔在出兵点附近自动生成1个单位;
- 产出单位从**当前玩家出战卡组**中随机抽取,不从所有已解锁或全体角色中抽取;
- 随机概率按召唤费用分层:3费及以下概率较高,费用越高概率越低;具体概率以测试为准;
- 产出单位不消耗玩家法力,价值定位低于玩家主动召唤,避免完全替代正常出兵;
- 产出单位默认执行随机出兵点归属方当前的出兵目标;如无特殊设置,则默认目标为敌方主基地;
- 随机出兵点被对方完全翻转后,自动出兵权转移给对方。
## 四、单位系统
### 4.1 通用属性
所有单位统一包含:召唤费用、生命值、攻击力、攻击距离、移动速度、攻击优先级。
### 4.2 单位分类与定位
| 单位 | 费用档位 | 定位 | 核心特点 |
|------|----------|------|----------|
| 近战步兵 | 低费 | 前排肉盾/量产 | 血量适中,近战输出,优先攻击敌方单位 |
| 远程射手 | 中费 | 后排输出 | 身脆伤害高,需前排保护,克制无甲单位 |
| 重甲单位 | 高费 | 推线核心 | 高血量,伤害偏低,优先攻击建筑 |
| 范围法师 | 高额 | 群体输出 | 范围伤害,克制扎堆小兵,自身极脆 |
### 4.3 基础克制关系
步兵克制法师、射手克制步兵、重甲克制建筑、法师克制扎堆步兵,形成循环博弈。
## 五、核心战斗逻辑
### 5.1 单位AI优先级(固定不变)
单位自动寻敌,优先级从高到低:
1. 视野内的敌方单位(最高优先级,发现即停止移动并攻击)
2. 自身绑定的指定目标(水晶矿 / 随机出兵点 / 敌方基地)
3. 路径上的敌方防御塔
4. 敌方主基地
> 说明:敌方单位优先级永远最高,保证单位不会无视敌人强行拆矿/拆塔;敌人击杀后,自动继续前往原定目标。
### 5.2 出兵目标机制(方案B)
核心操作规则,**无驻守指令、无全局切换、无单位选中**,完全通过「召唤前预设目标」实现分兵。
1. 玩家在召唤单位前,先选择目标(进攻基地 / 进攻水晶矿 / 进攻随机出兵点);
2. 点击卡牌召唤后,**新出生的单位永久绑定该目标**,直到死亡;
3. 场上已存在的单位,目标不受后续按钮切换影响,始终执行出生时的任务;
4. 默认目标为「进攻敌方基地」,符合玩家基础直觉。
### 5.3 战斗行为示例
1. 玩家选择目标「水晶矿」,召唤2个步兵,步兵出生后自动向水晶矿移动;
2. 途中遭遇敌方单位,步兵立刻停止前进,优先攻击敌人;
3. 敌人被击杀后,步兵继续前往水晶矿,攻击水晶推进己方进度;
4. 玩家切换目标为「进攻随机出兵点」,再次召唤1个重甲,重甲出生后自动前往出兵点;
5. 此前的2个步兵仍继续攻击水晶矿,不受目标切换影响,实现分兵作战;
6. 随机出兵点归属己方期间,按固定间隔从玩家出战卡组中随机生成单位,生成单位默认前往敌方主基地。
## 六、UI布局与操作
### 6.1 整体布局
采用「上战场、下操作」的竖版经典结构,战斗视野完整,拇指可覆盖全部操作区。
┌──────────────────────────────┐
│ │
│ 战场区域(2.5D) │
│ │
│ │
│ │
├──────────────────────────────┤
│ 攻城堡 打水晶 打出兵点 │ ← 目标选择按钮组
├──────────────────────────────┤
│ 步兵 射手 重甲 法师 │ ← 单位召唤卡牌面板
└──────────────────────────────┘
### 6.2 目标选择按钮组
- 位置:召唤卡牌面板正上方,横向并排3个按钮;
- 交互:同一时间仅一个选中,选中态高亮显示,未选中置灰;
- 逻辑:仅决定「下一次召唤单位的目标」,不影响场上已存活单位;
- 可选目标为「攻城堡」「打水晶」「打出兵点」;
- 默认选中「攻城堡」,开局无需额外操作即可出兵。
### 6.3 单位召唤卡牌面板
- 位置:屏幕最底部,横向排列单位卡牌;
- 交互:点击卡牌消耗对应法力,单位在己方城堡附近生成;
- 信息:卡牌显示单位图标、召唤费用,费用不足时置灰不可点击。
### 6.4 标准操作流程
1. 观察战场局势,决定当前波次的目标(抢水晶矿、抢随机出兵点或推基地);
2. 点击对应目标按钮,确认选中状态;
3. 点击单位卡牌,消耗法力召唤对应兵种;
4. 重复上述步骤,多批次分配不同目标的部队,形成多线作战。
## 七、单局对局流程
1. **开局阶段(0~30秒)**
双方基础法力回复,默认出兵冲基地试探;可选择抢先派兵争夺水晶矿或随机出兵点,建立经济或兵力优势。
2. **博弈阶段(30秒~1分30秒)**
围绕水晶矿与随机出兵点展开争夺,搭配兵种组合,用前排+后排阵容推进;通过分兵同时牵制资源据点与主线。
3. **决胜阶段(1分30秒~3分钟)**
积累足够兵力后集中推进,突破防御塔,集火敌方城堡;城堡血量归零则对局结束。
若3分钟未分胜负,按城堡剩余血量百分比判定胜负,防止无限拉扯。
## 八、核心策略博弈点
1. **资源节奏**:攒法力出高费重甲,还是持续低费小兵压制;
2. **据点取舍**:优先抢水晶矿提升长期法力回复,还是抢随机出兵点获得额外兵力,或全力一波速攻基地;
3. **分兵策略**:同时争夺水晶矿和随机出兵点,还是集中兵力主攻一路;
4. **兵种搭配**:前排抗线+后排输出的组合,避免被范围伤害一波清场。
## 九、开发落地建议
1. **寻路复用**:直接复用现有NavMesh寻路逻辑,仅需烘焙十字形可行走区域;
2. **AI简化**:保留原有单位战斗状态机,仅新增「出生目标绑定」字段,无需新增驻守状态;
3. **据点实现**:水晶矿与随机出兵点作为特殊可攻击目标,共用红蓝争夺进度数值;据点达到己方100%后开始产出,只有被对方完全推到0才翻转并转移产出;水晶矿提供法力回复加成,随机出兵点按费用权重从玩家出战卡组中自动生成单位;
4. **UI优先**:先实现底部目标按钮+召唤面板的核心交互,再扩展技能、特效等内容。
-386
View File
@@ -1,386 +0,0 @@
# 微信/抖音小游戏可行性方案:法师捕获召唤闯关
## 1. 项目定位
目标做一款适合微信小游戏、抖音小游戏的轻量 3D 闯关游戏。
核心卖点:
- 玩家扮演法师。
- 打倒或削弱敌人后,可以捕获敌人单位。
- 捕获后的单位可在战斗中召唤出来协助作战。
- 召唤单位消耗法力,法力会随时间自动恢复。
- 以短关卡推进,适合碎片化游玩和广告变现。
一句话概括:
> 轻量版“捕获 + 召唤 + 闯关”的 3D 卡通动作策略小游戏。
## 2. 核心玩法判断
这个方向可行,但不建议做成开放世界或复杂解谜大作。小游戏平台更适合:
- 单局时间短。
- 操作简单。
- 目标明确。
- 关卡推进快。
- 内容可以持续扩展。
因此推荐形态是:
> 俯视角 / 斜俯视角 3D 关卡制动作策略游戏。
玩家主要体验不是复杂操作,而是:
- 走位躲避。
- 普攻削血。
- 捕获敌人。
- 选择合适单位召唤。
- 用有限法力解决当前战斗。
## 3. MVP 核心循环
第一版只验证核心是否好玩,不做过多外围系统。
基础循环:
```text
进入关卡
→ 遇到敌人
→ 主角自动普攻削血
→ 敌人进入可捕获状态
→ 玩家捕获单位
→ 消耗法力召唤已捕获单位
→ 单位协助战斗
→ 通关获得奖励
→ 解锁/强化单位
→ 进入下一关
```
MVP 必须具备:
1. 主角移动。
2. 主角简单自动普攻。
3. 敌人血量与死亡。
4. 敌人残血捕获。
5. 单位收集列表。
6. 法力自动恢复。
7. 消耗法力召唤单位。
8. 至少 3 种敌人/可召唤单位。
9. 至少 5 个测试关卡。
10. 简单胜负条件。
## 4. 主角设计
### 4.1 主角是否需要攻击能力
需要,但第一版只做简单普攻。
理由:
- 没有普攻,玩家开局或单位死亡后会失去行动能力。
- 捕获系统需要玩家先削弱敌人。
- 普攻能保证玩家持续参与,而不是只等法力恢复。
- 主角不应成为主要输出,否则召唤系统价值会下降。
### 4.2 推荐定位
主角是“指挥型法师”:
- 能自保。
- 能削血。
- 能补刀。
- 真正的战斗主力来自召唤单位。
输出占比建议:
| 来源 | 占比 |
|---|---:|
| 主角普攻 | 30% |
| 召唤单位 | 60% |
| 法术/道具 | 10% |
### 4.3 第一版主角能力
- 移动:虚拟摇杆或拖拽移动。
- 普攻:自动锁定最近敌人,发射魔法弹。
- 捕获:敌人低血量时点击捕获按钮。
- 召唤:选择已捕获单位,在目标位置放置。
暂不做复杂连招、装备技能树和多主动技能。
## 5. 法力系统
法力是召唤单位的核心资源。
### 5.1 基础规则
- 法力随时间自动恢复。
- 不同单位消耗不同法力。
- 法力有上限。
- 召唤后立即扣除法力。
- 强单位消耗高,弱单位消耗低。
### 5.2 推荐初始数值
| 参数 | 建议值 |
|---|---:|
| 初始法力上限 | 10 |
| 初始法力恢复 | 1 点 / 秒 |
| 小怪召唤消耗 | 2-3 |
| 远程单位消耗 | 3-4 |
| 坦克单位消耗 | 4-5 |
| 精英单位消耗 | 6-8 |
### 5.3 设计目的
法力系统要制造选择:
- 是马上召唤两个小怪?
- 还是攒法力召唤一个强力单位?
- 是用坦克挡伤害?
- 还是用远程单位补输出?
## 6. 捕获系统
捕获是项目最有辨识度的玩法。
### 6.1 第一版捕获规则
推荐简单规则:
- 敌人血量低于 30% 时进入可捕获状态。
- 可捕获状态显示明显特效或图标。
- 玩家点击捕获按钮后尝试捕获。
- 普通敌人捕获成功率高。
- 精英敌人捕获成功率低或需要通关后解锁。
### 6.2 MVP 简化方案
第一版可以先做确定性捕获:
```text
敌人血量低于 30% → 点击捕获 → 100% 成功
```
这样方便验证玩法闭环。后续再加入概率、品质、道具等复杂机制。
### 6.3 捕获后的单位
捕获后进入单位池:
- 可以在当前关卡继续召唤。
- 也可以进入关卡外图鉴。
- 同类单位可以转化为升级材料。
## 7. 召唤单位设计
第一版建议只做 3-5 种单位,保证差异明显。
| 单位 | 定位 | 特点 | 法力消耗 |
|---|---|---|---:|
| 小史莱姆 | 低费近战 | 便宜、数量多、伤害低 | 2 |
| 蓝色怪物 | 近战战士 | 均衡输出与血量 | 3 |
| 石头怪 | 坦克 | 血厚、移速慢 | 5 |
| 弓箭手 | 远程输出 | 脆皮、站后排 | 4 |
| 冰霜怪 | 控制 | 攻击带减速 | 5 |
MVP 可以先做:
1. 近战小怪。
2. 坦克小怪。
3. 远程小怪。
只要三种单位的战斗感受不同,就能验证策略性。
## 8. 关卡结构
### 8.1 单关时长
小游戏适合短关卡:
| 类型 | 时长 |
|---|---:|
| 普通关 | 1-2 分钟 |
| 精英关 | 2-3 分钟 |
| Boss 关 | 3-5 分钟 |
### 8.2 第一版关卡类型
推荐先做闯关型:
- 每关一个小地图。
- 玩家从入口进入。
- 击败全部敌人或击败 Boss 后通关。
- 通关后获得金币、经验、单位碎片。
### 8.3 关卡变化方式
早期不需要大地图,靠以下内容制造变化:
- 敌人组合变化。
- 障碍物位置变化。
- 地形大小变化。
- 陷阱和机关。
- Boss 技能变化。
## 9. 成长系统
第一版成长不要太重,避免拖慢开发。
### 9.1 法师成长
- 最大生命值。
- 普攻伤害。
- 法力上限。
- 法力恢复速度。
- 捕获范围。
### 9.2 单位成长
- 单位等级。
- 生命值提升。
- 攻击力提升。
- 特殊效果增强。
### 9.3 图鉴系统
图鉴是很适合这个玩法的长期目标:
- 收集过的单位会点亮图鉴。
- 图鉴给少量属性加成。
- 稀有单位作为关卡目标。
## 10. 广告变现设计
这个玩法可以做广告变现,尤其适合激励视频广告。
### 10.1 推荐广告点
| 广告位 | 说明 | 推荐程度 |
|---|---|---|
| 失败复活 | 死亡后看广告复活一次 | 高 |
| 通关奖励翻倍 | 通关后金币/材料翻倍 | 高 |
| 额外捕获机会 | 捕获失败后看广告再试一次 | 中高 |
| 每日奖励翻倍 | 每日登录奖励翻倍 | 中 |
| 体力/门票补充 | 体力不足时看广告补充 | 中 |
| 临时法力加速 | 本关法力恢复短时间提升 | 中 |
### 10.2 不建议的广告
不建议做:
- 关卡中强制插屏。
- 每一两分钟强制广告。
- 战斗中频繁打断操作。
小游戏前期更适合激励广告,让玩家主动选择看广告。
## 11. 技术可行性
### 11.1 适合小游戏的原因
- 玩法可以做成小地图短关卡。
- 角色数量可控。
- 3D 卡通低模风格适合性能控制。
- 系统可以逐步扩展,不必一开始做大世界。
### 11.2 风险点
| 风险 | 说明 | 规避方式 |
|---|---|---|
| 操作复杂 | 移动、攻击、捕获、召唤同时存在 | 普攻自动化,玩家只管移动、捕获、召唤 |
| 单位太多导致混乱 | 召唤物数量过多会遮挡和卡顿 | 限制同时在场数量或总法力 |
| 关卡重复 | 只打怪容易腻 | 增加敌人组合、地形、Boss、词缀 |
| 平衡困难 | 捕获和召唤容易过强 | 用法力消耗、召唤上限、冷却控制 |
| 性能压力 | 小游戏平台性能有限 | 低模、少骨骼、少特效、限制同屏单位 |
### 11.3 性能建议
- 采用低模 3D 卡通风格。
- 同屏敌人 + 召唤物建议控制在 20 个以内。
- 单位动画简单化。
- 特效使用少量粒子或贴片动画。
- 地图使用简单 Mesh / Cube / Plane 组合。
- UI 和战斗逻辑尽量轻量。
## 12. 推荐开发阶段
### 阶段 1:核心原型
目标:验证“捕获 + 召唤”是否好玩。
内容:
- 一个测试地图。
- 一个主角。
- 三种敌人。
- 自动普攻。
- 残血捕获。
- 法力恢复。
- 召唤单位。
- 一个胜负流程。
### 阶段 2:关卡 MVP
目标:做出可连续玩的体验。
内容:
- 5-10 个关卡。
- 1 个 Boss。
- 简单成长系统。
- 单位图鉴雏形。
- 通关奖励。
### 阶段 3:小游戏商业化测试
目标:验证留存和广告点。
内容:
- 失败复活广告。
- 通关奖励翻倍广告。
- 每日奖励。
- 基础数据埋点。
### 阶段 4:内容扩展
目标:增强长期游玩。
内容:
- 更多单位。
- 更多 Boss。
- 关卡词缀。
- 每日挑战。
- 单位升级和稀有度。
## 13. 当前项目建议
当前项目还处于早期,建议不要急着做完整游戏系统。
最优先事项:
1. 确定角色控制方式。
2. 做一个 40x40 的测试地图。
3. 让蓝色怪物作为第一个敌人/召唤单位。
4. 做主角自动普攻。
5. 做敌人残血捕获。
6. 做法力条和召唤按钮。
第一阶段目标不是好看,而是验证:
> 玩家是否愿意为了捕获单位而继续打下一关。
## 14. 最终建议
这个项目方向可行,适合做小游戏。
建议坚持三个原则:
1. **主角简单**:移动 + 自动普攻 + 捕获 + 召唤。
2. **单位有差异**:近战、坦克、远程、控制逐步扩展。
3. **关卡短平快**:1-3 分钟一局,适合广告和碎片化游玩。
第一版不要追求复杂剧情、大地图和大量角色。先把“捕获敌人并召唤出来战斗”的核心爽点做出来,再决定是否扩展为更大的项目。