diff --git a/DungeonShooting_Godot/excel/excelFile/ActivityObject.xlsx b/DungeonShooting_Godot/excel/excelFile/ActivityObject.xlsx index 8c505f0..d929d56 100644 --- a/DungeonShooting_Godot/excel/excelFile/ActivityObject.xlsx +++ b/DungeonShooting_Godot/excel/excelFile/ActivityObject.xlsx Binary files differ diff --git a/DungeonShooting_Godot/excel/excelFile/Sound.xlsx b/DungeonShooting_Godot/excel/excelFile/Sound.xlsx index 11ed22b..f8dd720 100644 --- a/DungeonShooting_Godot/excel/excelFile/Sound.xlsx +++ b/DungeonShooting_Godot/excel/excelFile/Sound.xlsx Binary files differ diff --git a/DungeonShooting_Godot/excel/excelFile/Weapon.xlsx b/DungeonShooting_Godot/excel/excelFile/Weapon.xlsx index b7e8df3..dd5760f 100644 --- a/DungeonShooting_Godot/excel/excelFile/Weapon.xlsx +++ b/DungeonShooting_Godot/excel/excelFile/Weapon.xlsx Binary files differ diff --git a/DungeonShooting_Godot/prefab/prop/buff/Prop0001.tscn b/DungeonShooting_Godot/prefab/prop/buff/Prop0001.tscn new file mode 100644 index 0000000..d1a1f24 --- /dev/null +++ b/DungeonShooting_Godot/prefab/prop/buff/Prop0001.tscn @@ -0,0 +1,40 @@ +[gd_scene load_steps=7 format=3 uid="uid://cb4k0wmt3rhjc"] + +[ext_resource type="Script" path="res://src/game/activity/prop/buff/MoveSpeedBuff.cs" id="1_haxff"] +[ext_resource type="Shader" path="res://resource/material/Blend.gdshader" id="2_ao13f"] +[ext_resource type="SpriteFrames" uid="uid://wtvfyprel72y" path="res://resource/spriteFrames/Buff0001.tres" id="3_jv1el"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_mrkt4"] +resource_local_to_scene = true +shader = ExtResource("2_ao13f") +shader_parameter/blend = Color(0, 0, 0, 0.470588) +shader_parameter/schedule = 0.0 +shader_parameter/alpha = 0.0 + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_b6ii6"] +resource_local_to_scene = true +shader = ExtResource("2_ao13f") +shader_parameter/blend = Color(1, 1, 1, 1) +shader_parameter/schedule = 0.0 +shader_parameter/alpha = 1.0 + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_cpqup"] +size = Vector2(12, 10) + +[node name="Prop0001" type="CharacterBody2D" node_paths=PackedStringArray("ShadowSprite", "AnimatedSprite", "Collision")] +collision_layer = 4 +script = ExtResource("1_haxff") +ShadowSprite = NodePath("ShadowSprite") +AnimatedSprite = NodePath("AnimatedSprite") +Collision = NodePath("Collision") + +[node name="ShadowSprite" type="Sprite2D" parent="."] +z_index = -1 +material = SubResource("ShaderMaterial_mrkt4") + +[node name="AnimatedSprite" type="AnimatedSprite2D" parent="."] +material = SubResource("ShaderMaterial_b6ii6") +sprite_frames = ExtResource("3_jv1el") + +[node name="Collision" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_cpqup") diff --git a/DungeonShooting_Godot/prefab/role/Enemy0001.tscn b/DungeonShooting_Godot/prefab/role/Enemy0001.tscn index 8673633..d0e2f25 100644 --- a/DungeonShooting_Godot/prefab/role/Enemy0001.tscn +++ b/DungeonShooting_Godot/prefab/role/Enemy0001.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=7 format=3 uid="uid://dbrig6dq441wo"] [ext_resource type="PackedScene" uid="uid://cyrcv2jdgr8cf" path="res://prefab/role/RoleTemplate.tscn" id="1_5po38"] -[ext_resource type="Script" path="res://src/game/role/enemy/Enemy.cs" id="2_1plrq"] +[ext_resource type="Script" path="res://src/game/activity/role/enemy/Enemy.cs" id="2_1plrq"] [ext_resource type="Shader" path="res://resource/material/Blend.gdshader" id="3_x8agd"] [ext_resource type="SpriteFrames" uid="uid://cnctpyrn02rhd" path="res://resource/spriteFrames/Role1001.tres" id="4_qv8w5"] diff --git a/DungeonShooting_Godot/prefab/role/Role0001.tscn b/DungeonShooting_Godot/prefab/role/Role0001.tscn index 4717b1d..0ecb088 100644 --- a/DungeonShooting_Godot/prefab/role/Role0001.tscn +++ b/DungeonShooting_Godot/prefab/role/Role0001.tscn @@ -1,7 +1,7 @@ [gd_scene load_steps=7 format=3 uid="uid://cxhrcytrx0kcf"] [ext_resource type="PackedScene" uid="uid://cyrcv2jdgr8cf" path="res://prefab/role/RoleTemplate.tscn" id="1_10c2n"] -[ext_resource type="Script" path="res://src/game/role/Player.cs" id="2_7dmp4"] +[ext_resource type="Script" path="res://src/game/activity/role/Player.cs" id="2_i08u4"] [ext_resource type="Shader" path="res://resource/material/Blend.gdshader" id="3_rk4gg"] [ext_resource type="SpriteFrames" uid="uid://n11thtali6es" path="res://resource/spriteFrames/Role0001.tres" id="4_galcc"] @@ -21,7 +21,7 @@ [node name="Role0001" node_paths=PackedStringArray("HurtArea", "MountPoint", "BackMountPoint", "InteractiveArea", "ShadowSprite", "AnimatedSprite", "Collision") instance=ExtResource("1_10c2n")] collision_layer = 8 -script = ExtResource("2_7dmp4") +script = ExtResource("2_i08u4") HurtArea = NodePath("HurtArea") MountPoint = NodePath("MountPoint") BackMountPoint = NodePath("BackMountPoint") @@ -37,3 +37,5 @@ material = SubResource("ShaderMaterial_8hgu2") sprite_frames = ExtResource("4_galcc") frame_progress = 0.0995217 + +[node name="MountPoint2" type="Marker2D" parent="." index="7"] diff --git a/DungeonShooting_Godot/prefab/role/RoleTemplate.tscn b/DungeonShooting_Godot/prefab/role/RoleTemplate.tscn index 31c3ab7..e665ff2 100644 --- a/DungeonShooting_Godot/prefab/role/RoleTemplate.tscn +++ b/DungeonShooting_Godot/prefab/role/RoleTemplate.tscn @@ -1,7 +1,8 @@ [gd_scene load_steps=8 format=3 uid="uid://cyrcv2jdgr8cf"] [ext_resource type="Shader" path="res://resource/material/Blend.gdshader" id="1_xk5yk"] -[ext_resource type="Script" path="res://src/game/role/MountRotation.cs" id="2_5ddpw"] +[ext_resource type="Script" path="res://src/game/activity/role/MountRotation.cs" id="2_5ddpw"] + [sub_resource type="ShaderMaterial" id="ShaderMaterial_v2kfw"] resource_local_to_scene = true diff --git a/DungeonShooting_Godot/project.godot b/DungeonShooting_Godot/project.godot index 3e12ec5..20e8ef6 100644 --- a/DungeonShooting_Godot/project.godot +++ b/DungeonShooting_Godot/project.godot @@ -177,7 +177,7 @@ 2d_physics/layer_1="wall" 2d_physics/layer_2="bullet" -2d_physics/layer_3="props" +2d_physics/layer_3="prop" 2d_physics/layer_4="player" 2d_physics/layer_5="enemy" 2d_physics/layer_6="affiliation" diff --git a/DungeonShooting_Godot/resource/config/ActivityObject.json b/DungeonShooting_Godot/resource/config/ActivityObject.json index 8551ca6..69732fb 100644 --- a/DungeonShooting_Godot/resource/config/ActivityObject.json +++ b/DungeonShooting_Godot/resource/config/ActivityObject.json @@ -3,108 +3,152 @@ "Id": "role0001", "Type": 3, "Prefab": "res://prefab/role/Role0001.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "\u73A9\u5BB6" }, { "Id": "enemy0001", "Type": 4, "Prefab": "res://prefab/role/Enemy0001.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "\u654C\u4EBA" }, { "Id": "weapon0001", "Type": 5, "Prefab": "res://prefab/weapon/Weapon0001.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "weapon0002", "Type": 5, "Prefab": "res://prefab/weapon/Weapon0002.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "weapon0003", "Type": 5, "Prefab": "res://prefab/weapon/Weapon0003.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "weapon0004", "Type": 5, "Prefab": "res://prefab/weapon/Weapon0004.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "weapon0005", "Type": 5, "Prefab": "res://prefab/weapon/Weapon0005.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "weapon0006", "Type": 5, "Prefab": "res://prefab/weapon/Weapon0006.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "bullet0001", "Type": 6, "Prefab": "res://prefab/bullet/Bullet0001.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "bullet0002", "Type": 6, "Prefab": "res://prefab/bullet/Bullet0002.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "shell0001", "Type": 7, "Prefab": "res://prefab/shell/Shell0001.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "shell0002", "Type": 7, "Prefab": "res://prefab/shell/Shell0002.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "shell0003", "Type": 7, "Prefab": "res://prefab/shell/Shell0003.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "" }, { "Id": "effect0001", "Type": 8, "Prefab": "res://prefab/effect/activityObject/Effect0001.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "\u654C\u4EBA\u6B7B\u4EA1\u788E\u7247" }, { - "Id": "other_door_e", + "Id": "prop0001", "Type": 9, + "Prefab": "res://prefab/prop/buff/Prop0001.tscn", + "ItemName": "\u978B\u5B50", + "ItemDescription": "\u589E\u52A0\u79FB\u52A8\u901F\u5EA6", + "Remark": "\u589E\u52A0\u79FB\u901F\u7684buff" + }, + { + "Id": "other_door_e", + "Type": 99, "Prefab": "res://prefab/map/RoomDoor_E.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "\u5730\u7262\u623F\u95F4\u7684\u95E8(\u4E1C\u4FA7)" }, { "Id": "other_door_w", - "Type": 9, + "Type": 99, "Prefab": "res://prefab/map/RoomDoor_W.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "\u5730\u7262\u623F\u95F4\u7684\u95E8(\u897F\u4FA7)" }, { "Id": "other_door_s", - "Type": 9, + "Type": 99, "Prefab": "res://prefab/map/RoomDoor_S.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "\u5730\u7262\u623F\u95F4\u7684\u95E8(\u5357\u4FA7)" }, { "Id": "other_door_n", - "Type": 9, + "Type": 99, "Prefab": "res://prefab/map/RoomDoor_N.tscn", + "ItemName": "", + "ItemDescription": "", "Remark": "\u5730\u7262\u623F\u95F4\u7684\u95E8(\u5317\u4FA7)" } ] \ No newline at end of file diff --git a/DungeonShooting_Godot/resource/map/tileMaps/testGroup/inlet/Room1.tscn b/DungeonShooting_Godot/resource/map/tileMaps/testGroup/inlet/Room1.tscn index b971027..df5af1d 100644 --- a/DungeonShooting_Godot/resource/map/tileMaps/testGroup/inlet/Room1.tscn +++ b/DungeonShooting_Godot/resource/map/tileMaps/testGroup/inlet/Room1.tscn @@ -22,3 +22,10 @@ Type = 5 ItemExpression = "0003" WaveNumber = 2 + +[node name="ActivityMark8" type="Node2D" parent="."] +position = Vector2(126, 41) +script = ExtResource("3_m4jrh") +Type = 9 +ItemExpression = "0001" +WaveNumber = 2 diff --git a/DungeonShooting_Godot/resource/map/tiledata/testGroup/inlet/Room1.json b/DungeonShooting_Godot/resource/map/tiledata/testGroup/inlet/Room1.json index 952528a..6c3fba1 100644 --- a/DungeonShooting_Godot/resource/map/tiledata/testGroup/inlet/Room1.json +++ b/DungeonShooting_Godot/resource/map/tiledata/testGroup/inlet/Room1.json @@ -1,38 +1,38 @@ { "Position": { - "X": -1, - "Y": -1 + "X": -1, + "Y": -1 }, "Size": { - "X": 12, - "Y": 8 + "X": 12, + "Y": 8 }, "DoorAreaInfos": [], "NavigationList": [ - { - "Type": 0, - "Points": [ - { - "X": 8, - "Y": 8 - }, - { - "X": 152, - "Y": 8 - }, - { - "X": 152, - "Y": 96 - }, - { - "X": 8, - "Y": 96 - } - ] - } + { + "Type": 0, + "Points": [ + { + "X": 8, + "Y": 8 + }, + { + "X": 152, + "Y": 8 + }, + { + "X": 152, + "Y": 96 + }, + { + "X": 8, + "Y": 96 + } + ] + } ], "GroupName": "testGroup", "RoomType": 1, "FileName": "Room1", "Weight": 100 -} +} \ No newline at end of file diff --git a/DungeonShooting_Godot/resource/sprite/prop/buff/Buff0001.png b/DungeonShooting_Godot/resource/sprite/prop/buff/Buff0001.png new file mode 100644 index 0000000..61303db --- /dev/null +++ b/DungeonShooting_Godot/resource/sprite/prop/buff/Buff0001.png Binary files differ diff --git a/DungeonShooting_Godot/resource/sprite/prop/buff/Buff0001.png.import b/DungeonShooting_Godot/resource/sprite/prop/buff/Buff0001.png.import new file mode 100644 index 0000000..93bddc7 --- /dev/null +++ b/DungeonShooting_Godot/resource/sprite/prop/buff/Buff0001.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bls55gj8h3mgv" +path="res://.godot/imported/Buff0001.png-2d51f6e8c41b1c1eaee342d36314d326.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resource/sprite/prop/buff/Buff0001.png" +dest_files=["res://.godot/imported/Buff0001.png-2d51f6e8c41b1c1eaee342d36314d326.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/DungeonShooting_Godot/resource/spriteFrames/Buff0001.tres b/DungeonShooting_Godot/resource/spriteFrames/Buff0001.tres new file mode 100644 index 0000000..6a0ec5f --- /dev/null +++ b/DungeonShooting_Godot/resource/spriteFrames/Buff0001.tres @@ -0,0 +1,14 @@ +[gd_resource type="SpriteFrames" load_steps=2 format=3 uid="uid://wtvfyprel72y"] + +[ext_resource type="Texture2D" uid="uid://bls55gj8h3mgv" path="res://resource/sprite/prop/buff/Buff0001.png" id="1_scm06"] + +[resource] +animations = [{ +"frames": [{ +"duration": 1.0, +"texture": ExtResource("1_scm06") +}], +"loop": true, +"name": &"default", +"speed": 5.0 +}] diff --git a/DungeonShooting_Godot/src/config/ExcelConfig_ActivityObject.cs b/DungeonShooting_Godot/src/config/ExcelConfig_ActivityObject.cs index 12e9730..4c2bcc5 100644 --- a/DungeonShooting_Godot/src/config/ExcelConfig_ActivityObject.cs +++ b/DungeonShooting_Godot/src/config/ExcelConfig_ActivityObject.cs @@ -22,7 +22,8 @@ /// Bullet(子弹): 6 <br/> /// Shell(弹壳): 7 <br/> /// Effect(特效): 8 <br/> - /// Other(其它类型): 9 + /// Prop(道具): 9 <br/> + /// Other(其它类型): 99 /// </summary> [JsonInclude] public int Type; @@ -34,6 +35,18 @@ public string Prefab; /// <summary> + /// 物体名称 + /// </summary> + [JsonInclude] + public string ItemName; + + /// <summary> + /// 物体描述 + /// </summary> + [JsonInclude] + public string ItemDescription; + + /// <summary> /// 物体备注 /// </summary> [JsonInclude] @@ -48,6 +61,8 @@ inst.Id = Id; inst.Type = Type; inst.Prefab = Prefab; + inst.ItemName = ItemName; + inst.ItemDescription = ItemDescription; inst.Remark = Remark; return inst; } diff --git a/DungeonShooting_Godot/src/framework/activity/ActivityObject.cs b/DungeonShooting_Godot/src/framework/activity/ActivityObject.cs index 920f5b4..a689872 100644 --- a/DungeonShooting_Godot/src/framework/activity/ActivityObject.cs +++ b/DungeonShooting_Godot/src/framework/activity/ActivityObject.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; +using Config; using Godot; /// <summary> @@ -17,9 +18,9 @@ public static bool IsDebug { get; set; } /// <summary> - /// 当前物体类型id, 用于区分是否是同一种物体, 如果不是通过 ActivityObject.Create() 函数创建出来的对象那么 ItemId 为 null + /// 当前物体对应的配置数据, 如果不是通过 ActivityObject.Create() 函数创建出来的对象那么 ItemConfig 为 null /// </summary> - public string ItemId { get; private set; } + public ExcelConfig.ActivityObject ItemConfig { get; private set; } /// <summary> /// 是否是静态物体, 如果为true, 则会禁用移动处理 @@ -264,7 +265,7 @@ private static long _instanceIndex = 0; //初始化节点 - private void _InitNode(string itemId, World world) + private void _InitNode(RegisterActivityData activityData, World world) { #if TOOLS if (!Engine.IsEditorHint()) @@ -276,7 +277,7 @@ } #endif World = world; - ItemId = itemId; + ItemConfig = activityData.Config; Name = GetType().Name + (_instanceIndex++); _blendShaderMaterial = AnimatedSprite.Material as ShaderMaterial; _shadowBlendShaderMaterial = ShadowSprite.Material as ShaderMaterial; diff --git a/DungeonShooting_Godot/src/framework/activity/ActivityObject_Init.cs b/DungeonShooting_Godot/src/framework/activity/ActivityObject_Init.cs index 5e7e2a9..de9bda7 100644 --- a/DungeonShooting_Godot/src/framework/activity/ActivityObject_Init.cs +++ b/DungeonShooting_Godot/src/framework/activity/ActivityObject_Init.cs @@ -1,3 +1,5 @@ +using Config; + /// <summary> /// 根据配置表注册物体, 该类是自动生成的, 请不要手动编辑! /// </summary> @@ -9,64 +11,121 @@ public static class Ids { /// <summary> - /// 玩家 + /// 名称: <br/> + /// 备注: 玩家 /// </summary> public const string Id_role0001 = "role0001"; /// <summary> - /// 敌人 + /// 名称: <br/> + /// 备注: 敌人 /// </summary> public const string Id_enemy0001 = "enemy0001"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_weapon0001 = "weapon0001"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_weapon0002 = "weapon0002"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_weapon0003 = "weapon0003"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_weapon0004 = "weapon0004"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_weapon0005 = "weapon0005"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_weapon0006 = "weapon0006"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_bullet0001 = "bullet0001"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_bullet0002 = "bullet0002"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_shell0001 = "shell0001"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_shell0002 = "shell0002"; + /// <summary> + /// 名称: <br/> + /// 备注: + /// </summary> public const string Id_shell0003 = "shell0003"; /// <summary> - /// 敌人死亡碎片 + /// 名称: <br/> + /// 备注: 敌人死亡碎片 /// </summary> public const string Id_effect0001 = "effect0001"; /// <summary> - /// 地牢房间的门(东侧) + /// 名称: 鞋子 <br/> + /// 备注: 增加移速的buff + /// </summary> + public const string Id_prop0001 = "prop0001"; + /// <summary> + /// 名称: <br/> + /// 备注: 地牢房间的门(东侧) /// </summary> public const string Id_other_door_e = "other_door_e"; /// <summary> - /// 地牢房间的门(西侧) + /// 名称: <br/> + /// 备注: 地牢房间的门(西侧) /// </summary> public const string Id_other_door_w = "other_door_w"; /// <summary> - /// 地牢房间的门(南侧) + /// 名称: <br/> + /// 备注: 地牢房间的门(南侧) /// </summary> public const string Id_other_door_s = "other_door_s"; /// <summary> - /// 地牢房间的门(北侧) + /// 名称: <br/> + /// 备注: 地牢房间的门(北侧) /// </summary> public const string Id_other_door_n = "other_door_n"; } private static void _InitRegister() { - _activityRegisterMap.Add("role0001", "res://prefab/role/Role0001.tscn"); - _activityRegisterMap.Add("enemy0001", "res://prefab/role/Enemy0001.tscn"); - _activityRegisterMap.Add("weapon0001", "res://prefab/weapon/Weapon0001.tscn"); - _activityRegisterMap.Add("weapon0002", "res://prefab/weapon/Weapon0002.tscn"); - _activityRegisterMap.Add("weapon0003", "res://prefab/weapon/Weapon0003.tscn"); - _activityRegisterMap.Add("weapon0004", "res://prefab/weapon/Weapon0004.tscn"); - _activityRegisterMap.Add("weapon0005", "res://prefab/weapon/Weapon0005.tscn"); - _activityRegisterMap.Add("weapon0006", "res://prefab/weapon/Weapon0006.tscn"); - _activityRegisterMap.Add("bullet0001", "res://prefab/bullet/Bullet0001.tscn"); - _activityRegisterMap.Add("bullet0002", "res://prefab/bullet/Bullet0002.tscn"); - _activityRegisterMap.Add("shell0001", "res://prefab/shell/Shell0001.tscn"); - _activityRegisterMap.Add("shell0002", "res://prefab/shell/Shell0002.tscn"); - _activityRegisterMap.Add("shell0003", "res://prefab/shell/Shell0003.tscn"); - _activityRegisterMap.Add("effect0001", "res://prefab/effect/activityObject/Effect0001.tscn"); - _activityRegisterMap.Add("other_door_e", "res://prefab/map/RoomDoor_E.tscn"); - _activityRegisterMap.Add("other_door_w", "res://prefab/map/RoomDoor_W.tscn"); - _activityRegisterMap.Add("other_door_s", "res://prefab/map/RoomDoor_S.tscn"); - _activityRegisterMap.Add("other_door_n", "res://prefab/map/RoomDoor_N.tscn"); + _activityRegisterMap.Add("role0001", new RegisterActivityData("res://prefab/role/Role0001.tscn", ExcelConfig.ActivityObject_Map["role0001"])); + _activityRegisterMap.Add("enemy0001", new RegisterActivityData("res://prefab/role/Enemy0001.tscn", ExcelConfig.ActivityObject_Map["enemy0001"])); + _activityRegisterMap.Add("weapon0001", new RegisterActivityData("res://prefab/weapon/Weapon0001.tscn", ExcelConfig.ActivityObject_Map["weapon0001"])); + _activityRegisterMap.Add("weapon0002", new RegisterActivityData("res://prefab/weapon/Weapon0002.tscn", ExcelConfig.ActivityObject_Map["weapon0002"])); + _activityRegisterMap.Add("weapon0003", new RegisterActivityData("res://prefab/weapon/Weapon0003.tscn", ExcelConfig.ActivityObject_Map["weapon0003"])); + _activityRegisterMap.Add("weapon0004", new RegisterActivityData("res://prefab/weapon/Weapon0004.tscn", ExcelConfig.ActivityObject_Map["weapon0004"])); + _activityRegisterMap.Add("weapon0005", new RegisterActivityData("res://prefab/weapon/Weapon0005.tscn", ExcelConfig.ActivityObject_Map["weapon0005"])); + _activityRegisterMap.Add("weapon0006", new RegisterActivityData("res://prefab/weapon/Weapon0006.tscn", ExcelConfig.ActivityObject_Map["weapon0006"])); + _activityRegisterMap.Add("bullet0001", new RegisterActivityData("res://prefab/bullet/Bullet0001.tscn", ExcelConfig.ActivityObject_Map["bullet0001"])); + _activityRegisterMap.Add("bullet0002", new RegisterActivityData("res://prefab/bullet/Bullet0002.tscn", ExcelConfig.ActivityObject_Map["bullet0002"])); + _activityRegisterMap.Add("shell0001", new RegisterActivityData("res://prefab/shell/Shell0001.tscn", ExcelConfig.ActivityObject_Map["shell0001"])); + _activityRegisterMap.Add("shell0002", new RegisterActivityData("res://prefab/shell/Shell0002.tscn", ExcelConfig.ActivityObject_Map["shell0002"])); + _activityRegisterMap.Add("shell0003", new RegisterActivityData("res://prefab/shell/Shell0003.tscn", ExcelConfig.ActivityObject_Map["shell0003"])); + _activityRegisterMap.Add("effect0001", new RegisterActivityData("res://prefab/effect/activityObject/Effect0001.tscn", ExcelConfig.ActivityObject_Map["effect0001"])); + _activityRegisterMap.Add("prop0001", new RegisterActivityData("res://prefab/prop/buff/Prop0001.tscn", ExcelConfig.ActivityObject_Map["prop0001"])); + _activityRegisterMap.Add("other_door_e", new RegisterActivityData("res://prefab/map/RoomDoor_E.tscn", ExcelConfig.ActivityObject_Map["other_door_e"])); + _activityRegisterMap.Add("other_door_w", new RegisterActivityData("res://prefab/map/RoomDoor_W.tscn", ExcelConfig.ActivityObject_Map["other_door_w"])); + _activityRegisterMap.Add("other_door_s", new RegisterActivityData("res://prefab/map/RoomDoor_S.tscn", ExcelConfig.ActivityObject_Map["other_door_s"])); + _activityRegisterMap.Add("other_door_n", new RegisterActivityData("res://prefab/map/RoomDoor_N.tscn", ExcelConfig.ActivityObject_Map["other_door_n"])); } } diff --git a/DungeonShooting_Godot/src/framework/activity/ActivityObject_Register.cs b/DungeonShooting_Godot/src/framework/activity/ActivityObject_Register.cs index a08c0e6..9c82e49 100644 --- a/DungeonShooting_Godot/src/framework/activity/ActivityObject_Register.cs +++ b/DungeonShooting_Godot/src/framework/activity/ActivityObject_Register.cs @@ -1,12 +1,25 @@ using System; using System.Collections.Generic; +using Config; using Godot; public partial class ActivityObject { + private class RegisterActivityData + { + public RegisterActivityData(string path, ExcelConfig.ActivityObject config) + { + Path = path; + Config = config; + } + + public string Path; + public ExcelConfig.ActivityObject Config; + } + //负责存放所有注册对象数据 - private static Dictionary<string, string> _activityRegisterMap = new Dictionary<string, string>(); + private static Dictionary<string, RegisterActivityData> _activityRegisterMap = new Dictionary<string, RegisterActivityData>(); private static bool _initState = false; /// <summary> @@ -34,10 +47,10 @@ throw new Exception("实例化 ActivityObject 前请先调用 'GameApplication.Instance.CreateNewWorld()' 初始化 World 对象"); } - if (_activityRegisterMap.TryGetValue(itemId, out var path)) + if (_activityRegisterMap.TryGetValue(itemId, out var config)) { - var instance = ResourceManager.LoadAndInstantiate<ActivityObject>(path); - instance._InitNode(itemId, world); + var instance = ResourceManager.LoadAndInstantiate<ActivityObject>(config.Path); + instance._InitNode(config, world); return instance; } GD.PrintErr("创建实例失败, 未找到id为'" + itemId + "'的物体!"); diff --git a/DungeonShooting_Godot/src/framework/activity/CheckInteractiveResult.cs b/DungeonShooting_Godot/src/framework/activity/CheckInteractiveResult.cs index eb8676b..c528baa 100644 --- a/DungeonShooting_Godot/src/framework/activity/CheckInteractiveResult.cs +++ b/DungeonShooting_Godot/src/framework/activity/CheckInteractiveResult.cs @@ -25,4 +25,18 @@ { Target = target; } + + public CheckInteractiveResult(ActivityObject target, bool canInteractive) + { + Target = target; + CanInteractive = canInteractive; + } + + public CheckInteractiveResult(ActivityObject target, bool canInteractive, string message, string showIcon) + { + Target = target; + CanInteractive = canInteractive; + Message = message; + ShowIcon = showIcon; + } } \ No newline at end of file diff --git a/DungeonShooting_Godot/src/framework/generator/ExcelGenerator.cs b/DungeonShooting_Godot/src/framework/generator/ExcelGenerator.cs index 26d9357..86f1516 100644 --- a/DungeonShooting_Godot/src/framework/generator/ExcelGenerator.cs +++ b/DungeonShooting_Godot/src/framework/generator/ExcelGenerator.cs @@ -43,18 +43,18 @@ foreach (var item in array) { var id = item["Id"]; + var name = item["ItemName"] + ""; var remark = item["Remark"] + ""; - if (!string.IsNullOrEmpty(remark)) - { - code1 += $" /// <summary>\n"; - code1 += $" /// {remark}\n"; - code1 += $" /// </summary>\n"; - } + code1 += $" /// <summary>\n"; + code1 += $" /// 名称: {name} <br/>\n"; + code1 += $" /// 备注: {remark.Replace("\n", " <br/>\n /// ")}\n"; + code1 += $" /// </summary>\n"; code1 += $" public const string Id_{id} = \"{id}\";\n"; - code2 += $" _activityRegisterMap.Add(\"{id}\", \"{item["Prefab"]}\");\n"; + code2 += $" _activityRegisterMap.Add(\"{id}\", new RegisterActivityData(\"{item["Prefab"]}\", ExcelConfig.ActivityObject_Map[\"{id}\"]));\n"; } - var str = $"/// <summary>\n"; + var str = $"using Config;\n\n"; + str += $"/// <summary>\n"; str += $"/// 根据配置表注册物体, 该类是自动生成的, 请不要手动编辑!\n"; str += $"/// </summary>\n"; str += $"public partial class ActivityObject\n"; diff --git a/DungeonShooting_Godot/src/framework/map/AffiliationArea.cs b/DungeonShooting_Godot/src/framework/map/AffiliationArea.cs index 89cf6e0..337dab2 100644 --- a/DungeonShooting_Godot/src/framework/map/AffiliationArea.cs +++ b/DungeonShooting_Godot/src/framework/map/AffiliationArea.cs @@ -52,7 +52,7 @@ Monitoring = true; Monitorable = false; CollisionLayer = PhysicsLayer.None; - CollisionMask = PhysicsLayer.Props | PhysicsLayer.Player | PhysicsLayer.Enemy | PhysicsLayer.Shell | PhysicsLayer.Throwing; + CollisionMask = PhysicsLayer.Prop | PhysicsLayer.Player | PhysicsLayer.Enemy | PhysicsLayer.Shell | PhysicsLayer.Throwing; BodyEntered += OnBodyEntered; } diff --git a/DungeonShooting_Godot/src/game/PhysicsLayer.cs b/DungeonShooting_Godot/src/game/PhysicsLayer.cs index c1c53ea..a96e34d 100644 --- a/DungeonShooting_Godot/src/game/PhysicsLayer.cs +++ b/DungeonShooting_Godot/src/game/PhysicsLayer.cs @@ -18,7 +18,7 @@ /// <summary> /// 道具 /// </summary> - public const uint Props = 0b100; + public const uint Prop = 0b100; /// <summary> /// 玩家 /// </summary> diff --git a/DungeonShooting_Godot/src/game/activity/package/Holster.cs b/DungeonShooting_Godot/src/game/activity/package/Holster.cs index a487f64..df5dc56 100644 --- a/DungeonShooting_Godot/src/game/activity/package/Holster.cs +++ b/DungeonShooting_Godot/src/game/activity/package/Holster.cs @@ -141,7 +141,7 @@ for (var i = 0; i < Weapons.Length; i++) { var item = Weapons[i]; - if (item != null && item.ItemId == id) + if (item != null && item.ItemConfig.Id == id) { return i; } diff --git a/DungeonShooting_Godot/src/game/activity/prop/Prop.cs b/DungeonShooting_Godot/src/game/activity/prop/Prop.cs new file mode 100644 index 0000000..5962d3b --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/prop/Prop.cs @@ -0,0 +1,8 @@ + +/// <summary> +/// 道具基类 +/// </summary> +public abstract partial class Prop : ActivityObject +{ + +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/prop/buff/Buff.cs b/DungeonShooting_Godot/src/game/activity/prop/buff/Buff.cs new file mode 100644 index 0000000..1ca6d54 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/prop/buff/Buff.cs @@ -0,0 +1,43 @@ + +/// <summary> +/// 增益被动道具 +/// </summary> +public abstract partial class Buff : Prop +{ + /// <summary> + /// buff的拥有者 + /// </summary> + public Role Master { get; private set; } + + /// <summary> + /// 当被动被道具被拾起时调用 + /// </summary> + /// <param name="master">拾起该buff的角色</param> + protected abstract void OnPickUp(Role master); + + /// <summary> + /// 当被动道具被移除时调用 + /// </summary> + /// <param name="master">移除该buff的角色</param> + protected abstract void OnRemove(Role master); + + public override void Interactive(ActivityObject master) + { + if (master is Role role) + { + Pickup(); + Master = role; + role.PushBuff(this); + OnPickUp(role); + } + } + + public override CheckInteractiveResult CheckInteractive(ActivityObject master) + { + if (master is Player) + { + return new CheckInteractiveResult(this, true, "拾起道具", ResourcePath.resource_sprite_ui_icon_icon_pickup_png); + } + return base.CheckInteractive(master); + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/prop/buff/MoveSpeedBuff.cs b/DungeonShooting_Godot/src/game/activity/prop/buff/MoveSpeedBuff.cs new file mode 100644 index 0000000..7c4eb06 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/prop/buff/MoveSpeedBuff.cs @@ -0,0 +1,19 @@ + +using Godot; + +/// <summary> +/// 移速 buff +/// </summary> +[GlobalClass, Tool] +public partial class MoveSpeedBuff : Buff +{ + protected override void OnPickUp(Role master) + { + master.RoleState.MoveSpeed += 100; + } + + protected override void OnRemove(Role master) + { + master.RoleState.MoveSpeed -= 100; + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/CampEnum.cs b/DungeonShooting_Godot/src/game/activity/role/CampEnum.cs new file mode 100644 index 0000000..6954070 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/CampEnum.cs @@ -0,0 +1,10 @@ + +public enum CampEnum +{ + // 阵营1, 玩家 + Camp1, + // 阵营2, 敌人 + Camp2, + // 阵营3, 中立单位 + Camp3 +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/FaceDirection.cs b/DungeonShooting_Godot/src/game/activity/role/FaceDirection.cs new file mode 100644 index 0000000..5be28ef --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/FaceDirection.cs @@ -0,0 +1,8 @@ +/// <summary> +/// 脸的朝向 +/// </summary> +public enum FaceDirection +{ + Left = -1, + Right = 1, +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/MountRotation.cs b/DungeonShooting_Godot/src/game/activity/role/MountRotation.cs new file mode 100644 index 0000000..e265b10 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/MountRotation.cs @@ -0,0 +1,67 @@ + +using Godot; + +/// <summary> +/// 用于限定 Marker2D 节点的旋转角度 +/// </summary> +[Tool] +public partial class MountRotation : Marker2D +{ + /// <summary> + /// 吸附角度 + /// </summary> + private int _adsorption = 6; + + /// <summary> + /// 所在的角色 + /// </summary> + public Role Master { get; set; } + + /// <summary> + /// 当前节点真实的旋转角度, 角度制 + /// </summary> + public float RealRotationDegrees { get; private set; } + + /// <summary> + /// 当前节点真实的旋转角度, 弧度制 + /// </summary> + public float RealRotation => Mathf.DegToRad(RealRotationDegrees); + + /// <summary> + /// 设置看向的目标点 + /// </summary> + public void SetLookAt(Vector2 target) + { + var myPos = GlobalPosition; + var angle = Mathf.RadToDeg((target - myPos).Angle()); + + if (Master.Face == FaceDirection.Left) + { + if (angle < 0 && angle > -80) + { + angle = -80; + } + else if (angle >= 0 && angle < 80) + { + angle = 80; + } + } + else + { + angle = Mathf.Clamp(angle, -100, 100); + } + + RealRotationDegrees = angle; + + // if (Master.GlobalPosition.X >= target.X) + // { + // angle = -angle; + // } + GlobalRotationDegrees = AdsorptionAngle(angle); + } + + private float AdsorptionAngle(float angle) + { + return Mathf.Round(angle / _adsorption) * _adsorption; + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/Player.cs b/DungeonShooting_Godot/src/game/activity/role/Player.cs new file mode 100644 index 0000000..5da029d --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/Player.cs @@ -0,0 +1,249 @@ +using Godot; + + +/// <summary> +/// 玩家角色基类, 所有角色都必须继承该类 +/// </summary> +[Tool, GlobalClass] +public partial class Player : Role +{ + /// <summary> + /// 获取当前操作的角色 + /// </summary> + public static Player Current { get; private set; } + + /// <summary> + /// 移动加速度 + /// </summary> + public float Acceleration { get; set; } = 1500f; + + /// <summary> + /// 移动摩擦力 + /// </summary> + public float Friction { get; set; } = 800f; + + /// <summary> + /// 设置当前操作的玩家对象 + /// </summary> + public static void SetCurrentPlayer(Player player) + { + Current = player; + //设置相机和鼠标跟随玩家 + GameCamera.Main.SetFollowTarget(player); + GameApplication.Instance.Cursor.SetMountRole(player); + } + + public override void OnInit() + { + base.OnInit(); + + AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Prop | PhysicsLayer.Enemy; + Camp = CampEnum.Camp1; + + //让相机跟随玩家 + // var remoteTransform = new RemoteTransform2D(); + // AddChild(remoteTransform); + // MainCamera.Main.GlobalPosition = GlobalPosition; + // MainCamera.Main.ResetSmoothing(); + // remoteTransform.RemotePath = remoteTransform.GetPathTo(MainCamera.Main); + + MaxHp = 6; + Hp = 6; + MaxShield = 2; + Shield = 2; + + // debug用 + // Acceleration = 3000; + // Friction = 3000; + // MoveSpeed = 500; + // CollisionLayer = 0; + // CollisionMask = 0; + // GameCamera.Main.Zoom = new Vector2(0.5f, 0.5f); + } + + protected override void Process(float delta) + { + if (IsDie) + { + return; + } + base.Process(delta); + //脸的朝向 + if (LookTarget == null) + { + var gPos = GlobalPosition; + Vector2 mousePos = InputManager.CursorPosition; + if (mousePos.X > gPos.X && Face == FaceDirection.Left) + { + Face = FaceDirection.Right; + } + else if (mousePos.X < gPos.X && Face == FaceDirection.Right) + { + Face = FaceDirection.Left; + } + //枪口跟随鼠标 + MountPoint.SetLookAt(mousePos); + } + + if (InputManager.Exchange) //切换武器 + { + ExchangeNext(); + } + else if (InputManager.Throw) //扔掉武器 + { + ThrowWeapon(); + + // //测试用的, 所有敌人也扔掉武器 + // if (Affiliation != null) + // { + // var enemies = Affiliation.FindIncludeItems(o => + // { + // return o.CollisionWithMask(PhysicsLayer.Enemy); + // }); + // foreach (var activityObject in enemies) + // { + // if (activityObject is Enemy enemy) + // { + // enemy.ThrowWeapon(); + // } + // } + // } + } + else if (InputManager.Interactive) //互动物体 + { + TriggerInteractive(); + } + else if (InputManager.Reload) //换弹 + { + Reload(); + } + if (InputManager.Fire) //开火 + { + Attack(); + } + + if (Input.IsKeyPressed(Key.P)) + { + Hurt(1000, 0); + } + } + + protected override void PhysicsProcess(float delta) + { + if (IsDie) + { + return; + } + + base.PhysicsProcess(delta); + HandleMoveInput(delta); + //播放动画 + PlayAnim(); + } + + protected override int OnHandlerHurt(int damage) + { + //修改受到的伤害, 每次只受到1点伤害 + return 1; + } + + protected override void OnHit(int damage, bool realHarm) + { + //进入无敌状态 + if (realHarm) //真实伤害 + { + PlayInvincibleFlashing(1.5f); + } + else //护盾抵消掉的 + { + PlayInvincibleFlashing(0.5f); + } + } + + protected override void OnChangeHp(int hp) + { + //GameApplication.Instance.Ui.SetHp(hp); + EventManager.EmitEvent(EventEnum.OnPlayerHpChange, hp); + } + + protected override void OnChangeMaxHp(int maxHp) + { + //GameApplication.Instance.Ui.SetMaxHp(maxHp); + EventManager.EmitEvent(EventEnum.OnPlayerMaxHpChange, maxHp); + } + + protected override void ChangeInteractiveItem(CheckInteractiveResult result) + { + //派发互动对象改变事件 + EventManager.EmitEvent(EventEnum.OnPlayerChangeInteractiveItem, result); + } + + protected override void OnChangeShield(int shield) + { + //GameApplication.Instance.Ui.SetShield(shield); + EventManager.EmitEvent(EventEnum.OnPlayerShieldChange, shield); + } + + protected override void OnChangeMaxShield(int maxShield) + { + //GameApplication.Instance.Ui.SetMaxShield(maxShield); + EventManager.EmitEvent(EventEnum.OnPlayerMaxShieldChange, maxShield); + } + + protected override void OnDie() + { + GameCamera.Main.SetFollowTarget(null); + BasisVelocity = Vector2.Zero; + MoveController.ClearForce(); + UiManager.Open_Settlement(); + //GameApplication.Instance.World.ProcessMode = ProcessModeEnum.WhenPaused; + } + + //处理角色移动的输入 + private void HandleMoveInput(float delta) + { + //角色移动 + Vector2 dir = InputManager.MoveAxis; + // 移动. 如果移动的数值接近0(是用 摇杆可能出现 方向 可能会出现浮点),就friction的值 插值 到 0 + // 如果 有输入 就以当前速度,用acceleration 插值到 对应方向 * 最大速度 + if (Mathf.IsZeroApprox(dir.X)) + { + BasisVelocity = new Vector2(Mathf.MoveToward(BasisVelocity.X, 0, Friction * delta), BasisVelocity.Y); + } + else + { + BasisVelocity = new Vector2(Mathf.MoveToward(BasisVelocity.X, dir.X * RoleState.MoveSpeed, Acceleration * delta), + BasisVelocity.Y); + } + + if (Mathf.IsZeroApprox(dir.Y)) + { + BasisVelocity = new Vector2(BasisVelocity.X, Mathf.MoveToward(BasisVelocity.Y, 0, Friction * delta)); + } + else + { + BasisVelocity = new Vector2(BasisVelocity.X, + Mathf.MoveToward(BasisVelocity.Y, dir.Y * RoleState.MoveSpeed, Acceleration * delta)); + } + } + + // 播放动画 + private void PlayAnim() + { + if (BasisVelocity != Vector2.Zero) + { + if ((Face == FaceDirection.Right && BasisVelocity.X >= 0) || Face == FaceDirection.Left && BasisVelocity.X <= 0) //向前走 + { + AnimatedSprite.Play(AnimatorNames.Run); + } + else if ((Face == FaceDirection.Right && BasisVelocity.X < 0) || Face == FaceDirection.Left && BasisVelocity.X > 0) //向后走 + { + AnimatedSprite.Play(AnimatorNames.ReverseRun); + } + } + else + { + AnimatedSprite.Play(AnimatorNames.Idle); + } + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/Role.cs b/DungeonShooting_Godot/src/game/activity/role/Role.cs new file mode 100644 index 0000000..bc091b0 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/Role.cs @@ -0,0 +1,742 @@ +using System.Collections; +using System.Collections.Generic; +using Godot; + +/// <summary> +/// 角色基类 +/// </summary> +public abstract partial class Role : ActivityObject +{ + /// <summary> + /// 是否是 Ai + /// </summary> + public bool IsAi { get; protected set; } = false; + + /// <summary> + /// 角色属性 + /// </summary> + public RoleState RoleState { get; } = new RoleState(); + + /// <summary> + /// 默认攻击对象层级 + /// </summary> + public const uint DefaultAttackLayer = PhysicsLayer.Player | PhysicsLayer.Enemy | PhysicsLayer.Wall | PhysicsLayer.Prop; + + /// <summary> + /// 伤害区域 + /// </summary> + [Export, ExportFillNode] + public Area2D HurtArea { get; set; } + + /// <summary> + /// 所属阵营 + /// </summary> + public CampEnum Camp; + + /// <summary> + /// 攻击目标的碰撞器所属层级, 数据源自于: PhysicsLayer + /// </summary> + public uint AttackLayer { get; set; } = PhysicsLayer.Wall; + + // /// <summary> + // /// 携带的道具包裹 + // /// </summary> + // public List<object> PropsPack { get; } = new List<object>(); + + /// <summary> + /// 角色携带的武器袋 + /// </summary> + public Holster Holster { get; private set; } + + /// <summary> + /// 武器挂载点 + /// </summary> + [Export, ExportFillNode] + public MountRotation MountPoint { get; set; } + /// <summary> + /// 背后武器的挂载点 + /// </summary> + [Export, ExportFillNode] + public Marker2D BackMountPoint { get; set; } + + /// <summary> + /// 互动碰撞区域 + /// </summary> + [Export, ExportFillNode] + public Area2D InteractiveArea { get; set; } + + /// <summary> + /// 脸的朝向 + /// </summary> + public FaceDirection Face { get => _face; set => SetFace(value); } + private FaceDirection _face; + + /// <summary> + /// 是否死亡 + /// </summary> + public bool IsDie { get; private set; } + + /// <summary> + /// 血量 + /// </summary> + public int Hp + { + get => _hp; + protected set + { + int temp = _hp; + _hp = value; + if (temp != _hp) OnChangeHp(_hp); + } + } + private int _hp = 20; + + /// <summary> + /// 最大血量 + /// </summary> + public int MaxHp + { + get => _maxHp; + protected set + { + int temp = _maxHp; + _maxHp = value; + //护盾值改变 + if (temp != _maxHp) OnChangeMaxHp(_maxHp); + } + } + private int _maxHp = 20; + + /// <summary> + /// 当前护盾值 + /// </summary> + public int Shield + { + get => _shield; + protected set + { + int temp = _shield; + _shield = value; + //护盾被破坏 + if (temp > 0 && _shield <= 0) OnShieldDestroy(); + //护盾值改变 + if (temp != _shield) OnChangeShield(_shield); + } + } + private int _shield = 0; + + /// <summary> + /// 最大护盾值 + /// </summary> + public int MaxShield + { + get => _maxShield; + protected set + { + int temp = _maxShield; + _maxShield = value; + if (temp != _maxShield) OnChangeMaxShield(_maxShield); + } + } + private int _maxShield = 0; + + /// <summary> + /// 单格护盾恢复时间 + /// </summary> + private float ShieldRecoveryTime { get; set; } = 8; + + /// <summary> + /// 无敌状态 + /// </summary> + public bool Invincible + { + get => _invincible; + set + { + if (_invincible != value) + { + if (value) //无敌状态 + { + HurtArea.CollisionLayer = _currentLayer; + _flashingInvincibleTimer = -1; + _flashingInvincibleFlag = false; + } + else //正常状态 + { + HurtArea.CollisionLayer = _currentLayer; + SetBlendAlpha(1); + } + } + + _invincible = value; + } + } + + private bool _invincible = false; + + /// <summary> + /// 当前角色所看向的对象, 也就是枪口指向的对象 + /// </summary> + public ActivityObject LookTarget { get; set; } + + //初始缩放 + private Vector2 _startScale; + //所有角色碰撞的道具 + private readonly List<ActivityObject> _interactiveItemList = new List<ActivityObject>(); + + private CheckInteractiveResult _tempResultData; + private uint _currentLayer; + //闪烁计时器 + private float _flashingInvincibleTimer = -1; + //闪烁状态 + private bool _flashingInvincibleFlag = false; + //闪烁动画协程id + private long _invincibleFlashingId = -1; + //护盾恢复计时器 + private float _shieldRecoveryTimer = 0; + + /// <summary> + /// 可以互动的道具 + /// </summary> + public ActivityObject InteractiveItem { get; private set; } + + /// <summary> + /// 当血量改变时调用 + /// </summary> + protected virtual void OnChangeHp(int hp) + { + } + + /// <summary> + /// 当最大血量改变时调用 + /// </summary> + protected virtual void OnChangeMaxHp(int maxHp) + { + } + + /// <summary> + /// 护盾值改变时调用 + /// </summary> + protected virtual void OnChangeShield(int shield) + { + } + + /// <summary> + /// 最大护盾值改变时调用 + /// </summary> + protected virtual void OnChangeMaxShield(int maxShield) + { + } + + /// <summary> + /// 当护盾被破坏时调用 + /// </summary> + protected virtual void OnShieldDestroy() + { + } + + /// <summary> + /// 当受伤时调用 + /// </summary> + /// <param name="damage">受到的伤害</param> + /// <param name="realHarm">是否受到真实伤害, 如果为false, 则表示该伤害被互动格挡掉了</param> + protected virtual void OnHit(int damage, bool realHarm) + { + } + + /// <summary> + /// 受到伤害时调用, 用于改变受到的伤害值 + /// </summary> + /// <param name="damage">受到的伤害</param> + protected virtual int OnHandlerHurt(int damage) + { + return damage; + } + + /// <summary> + /// 当可互动的物体改变时调用, result 参数为 null 表示变为不可互动 + /// </summary> + /// <param name="result">检测是否可互动时的返回值</param> + protected virtual void ChangeInteractiveItem(CheckInteractiveResult result) + { + } + + /// <summary> + /// 死亡时调用 + /// </summary> + protected virtual void OnDie() + { + } + + public override void OnInit() + { + Holster = new Holster(this); + _startScale = Scale; + MountPoint.Master = this; + + HurtArea.CollisionLayer = CollisionLayer; + HurtArea.CollisionMask = 0; + _currentLayer = HurtArea.CollisionLayer; + + Face = FaceDirection.Right; + + //连接互动物体信号 + InteractiveArea.BodyEntered += _OnPropsEnter; + InteractiveArea.BodyExited += _OnPropsExit; + } + + protected override void Process(float delta) + { + //看向目标 + if (LookTarget != null) + { + Vector2 pos = LookTarget.GlobalPosition; + //脸的朝向 + var gPos = GlobalPosition; + if (pos.X > gPos.X && Face == FaceDirection.Left) + { + Face = FaceDirection.Right; + } + else if (pos.X < gPos.X && Face == FaceDirection.Right) + { + Face = FaceDirection.Left; + } + //枪口跟随目标 + MountPoint.SetLookAt(pos); + } + + //检查可互动的道具 + bool findFlag = false; + for (int i = 0; i < _interactiveItemList.Count; i++) + { + var item = _interactiveItemList[i]; + if (item == null || item.IsDestroyed) + { + _interactiveItemList.RemoveAt(i--); + } + else + { + //找到可互动的道具了 + if (!findFlag) + { + var result = item.CheckInteractive(this); + if (result.CanInteractive) //可以互动 + { + findFlag = true; + if (InteractiveItem != item) //更改互动物体 + { + InteractiveItem = item; + ChangeInteractiveItem(result); + } + else if (result.ShowIcon != _tempResultData.ShowIcon) //切换状态 + { + ChangeInteractiveItem(result); + } + } + _tempResultData = result; + } + } + } + //没有可互动的道具 + if (!findFlag && InteractiveItem != null) + { + InteractiveItem = null; + ChangeInteractiveItem(null); + } + + //无敌状态, 播放闪烁动画 + if (Invincible) + { + _flashingInvincibleTimer -= delta; + if (_flashingInvincibleTimer <= 0) + { + _flashingInvincibleTimer = 0.15f; + if (_flashingInvincibleFlag) + { + _flashingInvincibleFlag = false; + SetBlendAlpha(0.7f); + } + else + { + _flashingInvincibleFlag = true; + SetBlendAlpha(0); + } + } + + _shieldRecoveryTimer = 0; + } + else //恢复护盾 + { + if (Shield < MaxShield) + { + _shieldRecoveryTimer += delta; + if (_shieldRecoveryTimer >= ShieldRecoveryTime) //时间到, 恢复 + { + Shield++; + _shieldRecoveryTimer = 0; + } + } + else + { + _shieldRecoveryTimer = 0; + } + } + } + + /// <summary> + /// 当武器放到后背时调用, 用于设置武器位置和角度 + /// </summary> + /// <param name="weapon">武器实例</param> + /// <param name="index">放入武器袋的位置</param> + public virtual void OnPutBackMount(Weapon weapon, int index) + { + if (index < 8) + { + if (index % 2 == 0) + { + weapon.Position = new Vector2(-4, 3); + weapon.RotationDegrees = 90 - (index / 2f) * 20; + weapon.Scale = new Vector2(-1, 1); + } + else + { + weapon.Position = new Vector2(4, 3); + weapon.RotationDegrees = 270 + (index - 1) / 2f * 20; + weapon.Scale = new Vector2(1, 1); + } + } + else + { + weapon.Visible = false; + } + } + + protected override void OnAffiliationChange() + { + //身上的武器的所属区域也得跟着变 + Holster.ForEach((weapon, i) => + { + if (AffiliationArea != null) + { + AffiliationArea.InsertItem(weapon); + } + else if (weapon.AffiliationArea != null) + { + weapon.AffiliationArea.RemoveItem(weapon); + } + }); + } + + /// <summary> + /// 获取当前角色的中心点坐标 + /// </summary> + public Vector2 GetCenterPosition() + { + return MountPoint.GlobalPosition; + } + + /// <summary> + /// 使角色看向指定的坐标, + /// 注意, 调用该函数会清空 LookTarget, 因为拥有 LookTarget 时也会每帧更新玩家视野位置 + /// </summary> + /// <param name="pos"></param> + public void LookTargetPosition(Vector2 pos) + { + LookTarget = null; + //脸的朝向 + var gPos = GlobalPosition; + if (pos.X > gPos.X && Face == FaceDirection.Left) + { + Face = FaceDirection.Right; + } + else if (pos.X < gPos.X && Face == FaceDirection.Right) + { + Face = FaceDirection.Left; + } + //枪口跟随目标 + MountPoint.SetLookAt(pos); + } + + /// <summary> + /// 判断指定坐标是否在角色视野方向 + /// </summary> + public bool IsPositionInForward(Vector2 pos) + { + var gps = GlobalPosition; + return (Face == FaceDirection.Left && pos.X <= gps.X) || + (Face == FaceDirection.Right && pos.X >= gps.X); + } + + /// <summary> + /// 返回所有武器是否弹药都打光了 + /// </summary> + public bool IsAllWeaponTotalAmmoEmpty() + { + foreach (var weapon in Holster.Weapons) + { + if (weapon != null && !weapon.IsTotalAmmoEmpty()) + { + return false; + } + } + + return true; + } + + /// <summary> + /// 拾起一个武器, 返回是否成功拾取, 如果不想立刻切换到该武器, exchange 请传 false + /// </summary> + /// <param name="weapon">武器对象</param> + /// <param name="exchange">是否立即切换到该武器, 默认 true </param> + public virtual bool PickUpWeapon(Weapon weapon, bool exchange = true) + { + if (Holster.PickupWeapon(weapon, exchange) != -1) + { + //从可互动队列中移除 + _interactiveItemList.Remove(weapon); + return true; + } + + return false; + } + + /// <summary> + /// 切换到下一个武器 + /// </summary> + public virtual void ExchangeNext() + { + Holster.ExchangeNext(); + } + + /// <summary> + /// 切换到上一个武器 + /// </summary> + public virtual void ExchangePrev() + { + Holster.ExchangePrev(); + } + + /// <summary> + /// 扔掉当前使用的武器, 切换到上一个武器 + /// </summary> + public virtual void ThrowWeapon() + { + ThrowWeapon(Holster.ActiveIndex); + } + + /// <summary> + /// 扔掉指定位置的武器 + /// </summary> + /// <param name="index">武器在武器袋中的位置</param> + public virtual void ThrowWeapon(int index) + { + var weapon = Holster.GetWeapon(index); + if (weapon == null) + { + return; + } + + var temp = weapon.AnimatedSprite.Position; + if (Face == FaceDirection.Left) + { + temp.Y = -temp.Y; + } + //var pos = GlobalPosition + temp.Rotated(weapon.GlobalRotation); + Holster.RemoveWeapon(index); + //播放抛出效果 + weapon.ThrowWeapon(this, GlobalPosition); + } + + /// <summary> + /// 返回是否存在可互动的物体 + /// </summary> + public bool HasInteractive() + { + return InteractiveItem != null; + } + + /// <summary> + /// 触发与碰撞的物体互动, 并返回与其互动的物体 + /// </summary> + public ActivityObject TriggerInteractive() + { + if (HasInteractive()) + { + var item = InteractiveItem; + item.Interactive(this); + return item; + } + + return null; + } + + /// <summary> + /// 触发换弹 + /// </summary> + public virtual void Reload() + { + if (Holster.ActiveWeapon != null) + { + Holster.ActiveWeapon.Reload(); + } + } + + /// <summary> + /// 触发攻击 + /// </summary> + public virtual void Attack() + { + if (Holster.ActiveWeapon != null) + { + Holster.ActiveWeapon.Trigger(); + } + } + + /// <summary> + /// 受到伤害, 如果是在碰撞信号处理函数中调用该函数, 请使用 CallDeferred 来延时调用, 否则很有可能导致报错 + /// </summary> + /// <param name="damage">伤害的量</param> + /// <param name="angle">角度</param> + public virtual void Hurt(int damage, float angle) + { + //受伤闪烁, 无敌状态 + if (Invincible) + { + return; + } + + //计算真正受到的伤害 + damage = OnHandlerHurt(damage); + if (damage <= 0) + { + return; + } + + var flag = Shield > 0; + if (flag) + { + Shield -= damage; + } + else + { + Hp -= damage; + //播放血液效果 + // var packedScene = ResourceManager.Load<PackedScene>(ResourcePath.prefab_effect_Blood_tscn); + // var blood = packedScene.Instance<Blood>(); + // blood.GlobalPosition = GlobalPosition; + // blood.Rotation = angle; + // GameApplication.Instance.Node3D.GetRoot().AddChild(blood); + } + OnHit(damage, !flag); + + //受伤特效 + PlayHitAnimation(); + + //死亡判定 + if (Hp <= 0) + { + //死亡 + if (!IsDie) + { + IsDie = true; + OnDie(); + } + } + } + + /// <summary> + /// 播放无敌状态闪烁动画 + /// </summary> + /// <param name="time">持续时间</param> + public void PlayInvincibleFlashing(float time) + { + Invincible = true; + if (_invincibleFlashingId >= 0) //上一个还没结束 + { + StopCoroutine(_invincibleFlashingId); + } + + _invincibleFlashingId = StartCoroutine(RunInvincibleFlashing(time)); + } + + /// <summary> + /// 停止无敌状态闪烁动画 + /// </summary> + public void StopInvincibleFlashing() + { + Invincible = false; + if (_invincibleFlashingId >= 0) + { + StopCoroutine(_invincibleFlashingId); + _invincibleFlashingId = -1; + } + } + + private IEnumerator RunInvincibleFlashing(float time) + { + yield return new WaitForSeconds(time); + _invincibleFlashingId = -1; + Invincible = false; + } + + /// <summary> + /// 设置脸的朝向 + /// </summary> + private void SetFace(FaceDirection face) + { + if (_face != face) + { + _face = face; + if (face == FaceDirection.Right) + { + RotationDegrees = 0; + Scale = _startScale; + } + else + { + RotationDegrees = 180; + Scale = new Vector2(_startScale.X, -_startScale.Y); + } + } + } + + /// <summary> + /// 连接信号: InteractiveArea.BodyEntered + /// 与物体碰撞 + /// </summary> + private void _OnPropsEnter(Node2D other) + { + if (other is ActivityObject propObject && !propObject.CollisionWithMask(PhysicsLayer.OnHand)) + { + if (!_interactiveItemList.Contains(propObject)) + { + _interactiveItemList.Add(propObject); + } + } + } + + /// <summary> + /// 连接信号: InteractiveArea.BodyExited + /// 物体离开碰撞区域 + /// </summary> + private void _OnPropsExit(Node2D other) + { + if (other is ActivityObject propObject) + { + if (_interactiveItemList.Contains(propObject)) + { + _interactiveItemList.Remove(propObject); + } + if (InteractiveItem == propObject) + { + InteractiveItem = null; + ChangeInteractiveItem(null); + } + } + } + + public void PushBuff(Buff buff) + { + + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/RoleState.cs b/DungeonShooting_Godot/src/game/activity/role/RoleState.cs new file mode 100644 index 0000000..08d3338 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/RoleState.cs @@ -0,0 +1,11 @@ + +/// <summary> +/// 角色属性类 +/// </summary> +public class RoleState +{ + /// <summary> + /// 移动速度 + /// </summary> + public float MoveSpeed = 120f; +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/Enemy.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/Enemy.cs new file mode 100644 index 0000000..d2200c6 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/Enemy.cs @@ -0,0 +1,459 @@ +#region 基础敌人设计思路 +/* +敌人有三种状态: +状态1: 未发现玩家, 视野不可穿墙, 该状态下敌人移动比较规律, 移动速度较慢, 一旦玩家进入视野或者听到玩家枪声, 立刻切换至状态3, 该房间的敌人不能再回到状态1 +状态2: 发现有玩家, 但不知道在哪, 视野不可穿墙, 该情况下敌人移动速度明显加快, 移动不规律, 一旦玩家进入视野或者听到玩家枪声, 立刻切换至状态3 +状态3: 明确知道玩家的位置, 视野允许穿墙, 移动速度与状态2一致, 进入该状态时, 敌人之间会相互告知玩家所在位置, 并朝着玩家位置开火, + 如果有墙格挡, 则有一定概率继续开火, 一旦玩家立刻敌人视野超哥一段时间, 敌人自动切换为状态2 + +敌人状态1只存在于少数房间内, 比如特殊房间, 大部分情况下敌人应该是状态2, 或者玩家进入房间时就被敌人发现 +*/ +#endregion + + +using Godot; + +/// <summary> +/// 基础敌人 +/// </summary> +[Tool, GlobalClass] +public partial class Enemy : Role +{ + /// <summary> + /// 敌人身上的状态机控制器 + /// </summary> + public StateController<Enemy, AiStateEnum> StateController { get; private set; } + + /// <summary> + /// 视野半径, 单位像素, 发现玩家后改视野范围可以穿墙 + /// </summary> + public float ViewRange { get; set; } = 250; + + /// <summary> + /// 发现玩家后的视野半径 + /// </summary> + public float TailAfterViewRange { get; set; } = 400; + + /// <summary> + /// 背后的视野半径, 单位像素 + /// </summary> + public float BackViewRange { get; set; } = 50; + + /// <summary> + /// 视野检测射线, 朝玩家打射线, 检测是否碰到墙 + /// </summary> + public RayCast2D ViewRay { get; private set; } + + /// <summary> + /// 导航代理 + /// </summary> + public NavigationAgent2D NavigationAgent2D { get; private set; } + + /// <summary> + /// 导航代理中点 + /// </summary> + public Marker2D NavigationPoint { get; private set; } + + //开火间隙时间 + private float _enemyAttackTimer = 0; + //目标在视野内的时间 + private float _targetInViewTime = 0; + + public override void OnInit() + { + base.OnInit(); + IsAi = true; + StateController = AddComponent<StateController<Enemy, AiStateEnum>>(); + + AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Prop | PhysicsLayer.Player; + Camp = CampEnum.Camp2; + + RoleState.MoveSpeed = 20; + + MaxHp = 20; + Hp = 20; + + //视野射线 + ViewRay = GetNode<RayCast2D>("ViewRay"); + NavigationPoint = GetNode<Marker2D>("NavigationPoint"); + NavigationAgent2D = NavigationPoint.GetNode<NavigationAgent2D>("NavigationAgent2D"); + + //PathSign = new PathSign(this, PathSignLength, GameApplication.Instance.Node3D.Player); + + //注册Ai状态机 + StateController.Register(new AiNormalState()); + StateController.Register(new AiProbeState()); + StateController.Register(new AiTailAfterState()); + StateController.Register(new AiFollowUpState()); + StateController.Register(new AiLeaveForState()); + StateController.Register(new AiSurroundState()); + StateController.Register(new AiFindAmmoState()); + + //默认状态 + StateController.ChangeStateInstant(AiStateEnum.AiNormal); + } + + public override void EnterTree() + { + base.EnterTree(); + if (!World.Enemy_InstanceList.Contains(this)) + { + World.Enemy_InstanceList.Add(this); + } + } + + public override void ExitTree() + { + base.ExitTree(); + World.Enemy_InstanceList.Remove(this); + } + + protected override void OnDie() + { + //扔掉所有武器 + var weapons = Holster.GetAndClearWeapon(); + for (var i = 0; i < weapons.Length; i++) + { + weapons[i].ThrowWeapon(this); + } + + var effPos = Position + new Vector2(0, -Altitude); + //血液特效 + var blood = ResourceManager.LoadAndInstantiate<AutoDestroyEffect>(ResourcePath.prefab_effect_activityObject_EnemyBloodEffect_tscn); + blood.Position = effPos - new Vector2(0, 12); + blood.AddToActivityRoot(RoomLayerEnum.NormalLayer); + + //创建敌人碎片 + var count = Utils.RandomRangeInt(3, 6); + for (var i = 0; i < count; i++) + { + var debris = Create(Ids.Id_effect0001); + debris.PutDown(effPos, RoomLayerEnum.NormalLayer); + debris.InheritVelocity(this); + } + + //派发敌人死亡信号 + EventManager.EmitEvent(EventEnum.OnEnemyDie, this); + Destroy(); + } + + protected override void Process(float delta) + { + base.Process(delta); + _enemyAttackTimer -= delta; + + //目标在视野内的时间 + var currState = StateController.CurrState; + if (currState == AiStateEnum.AiSurround || currState == AiStateEnum.AiFollowUp) + { + _targetInViewTime += delta; + } + else + { + _targetInViewTime = 0; + } + + EnemyPickUpWeapon(); + } + + protected override void OnHit(int damage, bool realHarm) + { + //受到伤害 + var state = StateController.CurrState; + if (state == AiStateEnum.AiNormal || state == AiStateEnum.AiProbe || state == AiStateEnum.AiLeaveFor) + { + StateController.ChangeState(AiStateEnum.AiTailAfter); + } + } + + /// <summary> + /// 返回地上的武器是否有可以拾取的, 也包含没有被其他敌人标记的武器 + /// </summary> + public bool CheckUsableWeaponInUnclaimed() + { + foreach (var unclaimedWeapon in World.Weapon_UnclaimedWeapons) + { + //判断是否能拾起武器, 条件: 相同的房间 + if (unclaimedWeapon.AffiliationArea == AffiliationArea) + { + if (!unclaimedWeapon.IsTotalAmmoEmpty()) + { + if (!unclaimedWeapon.HasSign(SignNames.AiFindWeaponSign)) + { + return true; + } + else + { + //判断是否可以移除该标记 + var enemy = unclaimedWeapon.GetSign<Enemy>(SignNames.AiFindWeaponSign); + if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁 + { + unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign); + return true; + } + else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了 + { + unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign); + return true; + } + } + } + } + } + + return false; + } + + /// <summary> + /// 寻找可用的武器 + /// </summary> + public Weapon FindTargetWeapon() + { + Weapon target = null; + var position = Position; + foreach (var weapon in World.Weapon_UnclaimedWeapons) + { + //判断是否能拾起武器, 条件: 相同的房间, 或者当前房间目前没有战斗, 或者不在战斗房间 + if (weapon.AffiliationArea == AffiliationArea) + { + //还有弹药 + if (!weapon.IsTotalAmmoEmpty()) + { + //查询是否有其他敌人标记要拾起该武器 + if (weapon.HasSign(SignNames.AiFindWeaponSign)) + { + var enemy = weapon.GetSign<Enemy>(SignNames.AiFindWeaponSign); + if (enemy == this) //就是自己标记的 + { + + } + else if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁 + { + weapon.RemoveSign(SignNames.AiFindWeaponSign); + } + else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了 + { + weapon.RemoveSign(SignNames.AiFindWeaponSign); + } + else //放弃这把武器 + { + continue; + } + } + + if (target == null) //第一把武器 + { + target = weapon; + } + else if (target.Position.DistanceSquaredTo(position) > + weapon.Position.DistanceSquaredTo(position)) //距离更近 + { + target = weapon; + } + } + } + } + + return target; + } + + /// <summary> + /// 检查是否能切换到 AiStateEnum.AiLeaveFor 状态 + /// </summary> + /// <returns></returns> + public bool CanChangeLeaveFor() + { + if (!World.Enemy_IsFindTarget) + { + return false; + } + + var currState = StateController.CurrState; + if (currState == AiStateEnum.AiNormal || currState == AiStateEnum.AiProbe) + { + //判断是否在同一个房间内 + return World.Enemy_FindTargetAffiliationSet.Contains(AffiliationArea); + } + + return false; + } + + /// <summary> + /// Ai触发的攻击 + /// </summary> + public void EnemyAttack(float delta) + { + var weapon = Holster.ActiveWeapon; + if (weapon != null) + { + if (weapon.IsTotalAmmoEmpty()) //当前武器弹药打空 + { + //切换到有子弹的武器 + var index = Holster.FindWeapon((we, i) => !we.IsTotalAmmoEmpty()); + if (index != -1) + { + Holster.ExchangeByIndex(index); + } + else //所有子弹打光 + { + + } + } + else if (weapon.Reloading) //换弹中 + { + + } + else if (weapon.IsAmmoEmpty()) //弹夹已经打空 + { + Reload(); + } + else if (_targetInViewTime >= weapon.Attribute.AiTargetLockingTime) //正常射击 + { + if (weapon.GetDelayedAttackTime() > 0) + { + Attack(); + } + else + { + if (weapon.Attribute.ContinuousShoot) //连发 + { + Attack(); + } + else //单发 + { + if (_enemyAttackTimer <= 0) + { + _enemyAttackTimer = 60f / weapon.Attribute.StartFiringSpeed; + Attack(); + } + } + } + } + } + } + + /// <summary> + /// 获取武器攻击范围 (最大距离值与最小距离的中间值) + /// </summary> + /// <param name="weight">从最小到最大距离的过渡量, 0 - 1, 默认 0.5</param> + public float GetWeaponRange(float weight = 0.5f) + { + if (Holster.ActiveWeapon != null) + { + var attribute = Holster.ActiveWeapon.Attribute; + return Mathf.Lerp(attribute.BulletMinDistance, attribute.BulletMaxDistance, weight); + } + + return 0; + } + + /// <summary> + /// 返回目标点是否在视野范围内 + /// </summary> + public bool IsInViewRange(Vector2 target) + { + var isForward = IsPositionInForward(target); + if (isForward) + { + if (GlobalPosition.DistanceSquaredTo(target) <= ViewRange * ViewRange) //没有超出视野半径 + { + return true; + } + } + + return false; + } + + /// <summary> + /// 返回目标点是否在跟随状态下的视野半径内 + /// </summary> + public bool IsInTailAfterViewRange(Vector2 target) + { + var isForward = IsPositionInForward(target); + if (isForward) + { + if (GlobalPosition.DistanceSquaredTo(target) <= TailAfterViewRange * TailAfterViewRange) //没有超出视野半径 + { + return true; + } + } + + return false; + } + + /// <summary> + /// 调用视野检测, 如果被墙壁和其它物体遮挡, 则返回被挡住视野的物体对象, 视野无阻则返回 null + /// </summary> + public bool TestViewRayCast(Vector2 target) + { + ViewRay.Enabled = true; + ViewRay.TargetPosition = ViewRay.ToLocal(target); + ViewRay.ForceRaycastUpdate(); + return ViewRay.IsColliding(); + } + + /// <summary> + /// 调用视野检测完毕后, 需要调用 TestViewRayCastOver() 来关闭视野检测射线 + /// </summary> + public void TestViewRayCastOver() + { + ViewRay.Enabled = false; + } + + /// <summary> + /// AI 拾起武器操作 + /// </summary> + private void EnemyPickUpWeapon() + { + //这几个状态不需要主动拾起武器操作 + var state = StateController.CurrState; + if (state == AiStateEnum.AiNormal) + { + return; + } + + //拾起地上的武器 + if (InteractiveItem is Weapon weapon) + { + if (Holster.ActiveWeapon == null) //手上没有武器, 无论如何也要拾起 + { + TriggerInteractive(); + return; + } + + //没弹药了 + if (weapon.IsTotalAmmoEmpty()) + { + return; + } + + var index = Holster.FindWeapon((we, i) => we.ItemConfig.Id == weapon.ItemConfig.Id); + if (index != -1) //与武器袋中武器类型相同, 补充子弹 + { + if (!Holster.GetWeapon(index).IsAmmoFull()) + { + TriggerInteractive(); + } + + return; + } + + // var index2 = Holster.FindWeapon((we, i) => + // we.Attribute.WeightType == weapon.Attribute.WeightType && we.IsTotalAmmoEmpty()); + var index2 = Holster.FindWeapon((we, i) => we.IsTotalAmmoEmpty()); + if (index2 != -1) //扔掉没子弹的武器 + { + ThrowWeapon(index2); + TriggerInteractive(); + return; + } + + // if (Holster.HasVacancy()) //有空位, 拾起武器 + // { + // TriggerInteractive(); + // return; + // } + } + } + +} diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/state/AIStateEnum.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AIStateEnum.cs new file mode 100644 index 0000000..6bb8db4 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AIStateEnum.cs @@ -0,0 +1,32 @@ + +public enum AiStateEnum +{ + /// <summary> + /// Ai 状态, 正常, 未发现目标 + /// </summary> + AiNormal, + /// <summary> + /// 发现目标, 但不知道在哪 + /// </summary> + AiProbe, + /// <summary> + /// 收到其他敌人通知, 前往发现目标的位置 + /// </summary> + AiLeaveFor, + /// <summary> + /// 发现目标, 目标不在视野内, 但是知道位置 + /// </summary> + AiTailAfter, + /// <summary> + /// 目标在视野内, 跟进目标, 如果距离在子弹有效射程内, 则开火 + /// </summary> + AiFollowUp, + /// <summary> + /// 距离足够近, 在目标附近随机移动 + /// </summary> + AiSurround, + /// <summary> + /// Ai 寻找弹药 + /// </summary> + AiFindAmmo, +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiFindAmmoState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiFindAmmoState.cs new file mode 100644 index 0000000..f1f6863 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiFindAmmoState.cs @@ -0,0 +1,152 @@ + +using Godot; + +/// <summary> +/// Ai 寻找弹药, 进入该状态需要在参数中传入目标武器对象 +/// </summary> +public class AiFindAmmoState : StateBase<Enemy, AiStateEnum> +{ + + private Weapon _target; + + //导航目标点刷新计时器 + private float _navigationUpdateTimer = 0; + private float _navigationInterval = 1f; + + private bool _isInTailAfterRange = false; + private float _tailAfterTimer = 0; + + public AiFindAmmoState() : base(AiStateEnum.AiFindAmmo) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + if (args.Length == 0) + { + GD.PrintErr("进入 AiStateEnum.AiFindAmmo 状态必须要把目标武器当成参数传过来"); + ChangeState(prev); + return; + } + + SetTargetWeapon((Weapon)args[0]); + _navigationUpdateTimer = 0; + _isInTailAfterRange = false; + _tailAfterTimer = 0; + + //标记武器 + _target.SetSign(SignNames.AiFindWeaponSign, Master); + } + + public override void Process(float delta) + { + if (!Master.IsAllWeaponTotalAmmoEmpty()) //已经有弹药了 + { + ChangeState(GetNextState()); + return; + } + + //更新目标位置 + if (_navigationUpdateTimer <= 0) + { + //每隔一段时间秒更改目标位置 + _navigationUpdateTimer = _navigationInterval; + var position = _target.GlobalPosition; + Master.NavigationAgent2D.TargetPosition = position; + } + else + { + _navigationUpdateTimer -= delta; + } + + var playerPos = Player.Current.GetCenterPosition(); + //枪口指向玩家 + Master.LookTargetPosition(playerPos); + + if (_target.IsDestroyed || _target.IsTotalAmmoEmpty()) //已经被销毁, 或者弹药已经被其他角色捡走 + { + //再去寻找其他武器 + SetTargetWeapon(Master.FindTargetWeapon()); + + if (_target == null) //也没有其他可用的武器了 + { + ChangeState(GetNextState()); + } + } + else if (_target.Master == Master) //已经被自己拾起 + { + ChangeState(GetNextState()); + } + else if (_target.Master != null) //武器已经被其他角色拾起! + { + //再去寻找其他武器 + SetTargetWeapon(Master.FindTargetWeapon()); + + if (_target == null) //也没有其他可用的武器了 + { + ChangeState(GetNextState()); + } + } + else + { + //检测目标没有超出跟随视野距离 + _isInTailAfterRange = Master.IsInTailAfterViewRange(playerPos); + if (_isInTailAfterRange) + { + _tailAfterTimer = 0; + } + else + { + _tailAfterTimer += delta; + } + + //向武器移动 + if (!Master.NavigationAgent2D.IsNavigationFinished()) + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = + (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + else + { + Master.BasisVelocity = Vector2.Zero; + } + } + } + + private AiStateEnum GetNextState() + { + return _tailAfterTimer > 10 ? AiStateEnum.AiNormal : AiStateEnum.AiTailAfter; + } + + private void SetTargetWeapon(Weapon weapon) + { + _target = weapon; + //设置目标点 + if (_target != null) + { + Master.NavigationAgent2D.TargetPosition = _target.GlobalPosition; + } + } + + public override void DebugDraw() + { + if (_target != null) + { + Master.DrawLine(Vector2.Zero, Master.ToLocal(_target.GlobalPosition), Colors.Purple); + + if (_tailAfterTimer <= 0) + { + Master.DrawLine(Vector2.Zero, Master.ToLocal(Player.Current.GetCenterPosition()), Colors.Orange); + } + else if (_tailAfterTimer <= 10) + { + Master.DrawLine(Vector2.Zero, Master.ToLocal(Player.Current.GetCenterPosition()), Colors.Blue); + } + + } + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiFollowUpState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiFollowUpState.cs new file mode 100644 index 0000000..1e253ea --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiFollowUpState.cs @@ -0,0 +1,126 @@ + +using Godot; + +/// <summary> +/// 目标在视野内, 跟进目标, 如果距离在子弹有效射程内, 则开火 +/// </summary> +public class AiFollowUpState : StateBase<Enemy, AiStateEnum> +{ + + /// <summary> + /// 目标是否在视野内 + /// </summary> + public bool IsInView; + + //导航目标点刷新计时器 + private float _navigationUpdateTimer = 0; + private float _navigationInterval = 0.3f; + + public AiFollowUpState() : base(AiStateEnum.AiFollowUp) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + _navigationUpdateTimer = 0; + IsInView = true; + } + + public override void Process(float delta) + { + //先检查弹药是否打光 + if (Master.IsAllWeaponTotalAmmoEmpty()) + { + //再寻找是否有可用的武器 + var targetWeapon = Master.FindTargetWeapon(); + if (targetWeapon != null) + { + ChangeState(AiStateEnum.AiFindAmmo, targetWeapon); + return; + } + else + { + //切换到随机移动状态 + ChangeState(AiStateEnum.AiSurround); + } + } + + var playerPos = Player.Current.GetCenterPosition(); + + //更新玩家位置 + if (_navigationUpdateTimer <= 0) + { + //每隔一段时间秒更改目标位置 + _navigationUpdateTimer = _navigationInterval; + Master.NavigationAgent2D.TargetPosition = playerPos; + } + else + { + _navigationUpdateTimer -= delta; + } + + var masterPosition = Master.GlobalPosition; + + //是否在攻击范围内 + var inAttackRange = false; + + var weapon = Master.Holster.ActiveWeapon; + if (weapon != null) + { + inAttackRange = masterPosition.DistanceSquaredTo(playerPos) <= Mathf.Pow(Master.GetWeaponRange(0.7f), 2); + } + + //枪口指向玩家 + Master.LookTargetPosition(playerPos); + + if (!Master.NavigationAgent2D.IsNavigationFinished()) + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - masterPosition - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + else + { + Master.BasisVelocity = Vector2.Zero; + } + + //检测玩家是否在视野内 + if (Master.IsInTailAfterViewRange(playerPos)) + { + IsInView = !Master.TestViewRayCast(playerPos); + //关闭射线检测 + Master.TestViewRayCastOver(); + } + else + { + IsInView = false; + } + + if (IsInView) + { + if (inAttackRange) //在攻击范围内 + { + //发起攻击 + Master.EnemyAttack(delta); + + //距离够近, 可以切换到环绕模式 + if (Master.GlobalPosition.DistanceSquaredTo(playerPos) <= Mathf.Pow(weapon.Attribute.BulletMinDistance, 2) * 0.7f) + { + ChangeState(AiStateEnum.AiSurround); + } + } + } + else + { + ChangeState(AiStateEnum.AiTailAfter); + } + } + + public override void DebugDraw() + { + var playerPos = Player.Current.GetCenterPosition(); + Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Red); + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiLeaveForState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiLeaveForState.cs new file mode 100644 index 0000000..bb1c761 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiLeaveForState.cs @@ -0,0 +1,101 @@ + +using Godot; + +/// <summary> +/// 收到其他敌人通知, 前往发现目标的位置 +/// </summary> +public class AiLeaveForState : StateBase<Enemy, AiStateEnum> +{ + //导航目标点刷新计时器 + private float _navigationUpdateTimer = 0; + private float _navigationInterval = 0.3f; + + public AiLeaveForState() : base(AiStateEnum.AiLeaveFor) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + if (Master.World.Enemy_IsFindTarget) + { + Master.NavigationAgent2D.TargetPosition = Master.World.Enemy_FindTargetPosition; + } + else + { + ChangeState(prev); + return; + } + + //先检查弹药是否打光 + if (Master.IsAllWeaponTotalAmmoEmpty()) + { + //再寻找是否有可用的武器 + var targetWeapon = Master.FindTargetWeapon(); + if (targetWeapon != null) + { + ChangeState(AiStateEnum.AiFindAmmo, targetWeapon); + } + } + } + + public override void Process(float delta) + { + //这个状态下不会有攻击事件, 所以没必要每一帧检查是否弹药耗尽 + + //更新玩家位置 + if (_navigationUpdateTimer <= 0) + { + //每隔一段时间秒更改目标位置 + _navigationUpdateTimer = _navigationInterval; + Master.NavigationAgent2D.TargetPosition = Master.World.Enemy_FindTargetPosition; + } + else + { + _navigationUpdateTimer -= delta; + } + + if (!Master.NavigationAgent2D.IsNavigationFinished()) + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.LookTargetPosition(Master.World.Enemy_FindTargetPosition); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + else + { + Master.BasisVelocity = Vector2.Zero; + } + + var playerPos = Player.Current.GetCenterPosition(); + //检测玩家是否在视野内, 如果在, 则切换到 AiTargetInView 状态 + if (Master.IsInTailAfterViewRange(playerPos)) + { + if (!Master.TestViewRayCast(playerPos)) //看到玩家 + { + //关闭射线检测 + Master.TestViewRayCastOver(); + //切换成发现目标状态 + ChangeState(AiStateEnum.AiFollowUp); + return; + } + else + { + //关闭射线检测 + Master.TestViewRayCastOver(); + } + } + + //移动到目标掉了, 还没发现目标 + if (Master.NavigationAgent2D.IsNavigationFinished()) + { + ChangeState(AiStateEnum.AiNormal); + } + } + + public override void DebugDraw() + { + Master.DrawLine(Vector2.Zero, Master.ToLocal(Master.NavigationAgent2D.TargetPosition), Colors.Yellow); + } +} diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiNormalState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiNormalState.cs new file mode 100644 index 0000000..20cad58 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiNormalState.cs @@ -0,0 +1,181 @@ + +using Godot; + +/// <summary> +/// AI 正常状态 +/// </summary> +public class AiNormalState : StateBase<Enemy, AiStateEnum> +{ + //是否发现玩家 + private bool _isFindPlayer; + + //下一个运动的角度 + private Vector2 _nextPos; + + //是否移动结束 + private bool _isMoveOver; + + //上一次移动是否撞墙 + private bool _againstWall; + + //撞墙法线角度 + private float _againstWallNormalAngle; + + //移动停顿计时器 + private float _pauseTimer; + private bool _moveFlag; + + //上一帧位置 + private Vector2 _prevPos; + //卡在一个位置的时间 + private float _lockTimer; + + public AiNormalState() : base(AiStateEnum.AiNormal) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + _isFindPlayer = false; + _isMoveOver = true; + _againstWall = false; + _againstWallNormalAngle = 0; + _pauseTimer = 0; + _moveFlag = false; + } + + public override void Process(float delta) + { + //其他敌人发现玩家 + if (Master.CanChangeLeaveFor()) + { + ChangeState(AiStateEnum.AiLeaveFor); + return; + } + + if (_isFindPlayer) //已经找到玩家了 + { + //现临时处理, 直接切换状态 + ChangeState(AiStateEnum.AiTailAfter); + } + else //没有找到玩家 + { + //检测玩家 + var player = Player.Current; + //玩家中心点坐标 + var playerPos = player.GetCenterPosition(); + + if (Master.IsInViewRange(playerPos) && !Master.TestViewRayCast(playerPos)) //发现玩家 + { + //发现玩家 + _isFindPlayer = true; + } + else if (_pauseTimer >= 0) + { + Master.AnimatedSprite.Play(AnimatorNames.Idle); + _pauseTimer -= delta; + } + else if (_isMoveOver) //没发现玩家, 且已经移动完成 + { + RunOver(); + _isMoveOver = false; + } + else //移动中 + { + if (_lockTimer >= 1) //卡在一个点超过一秒 + { + RunOver(); + _isMoveOver = false; + _lockTimer = 0; + } + else if (Master.NavigationAgent2D.IsNavigationFinished()) //到达终点 + { + _pauseTimer = Utils.RandomRangeFloat(0.3f, 2f); + _isMoveOver = true; + _moveFlag = false; + Master.BasisVelocity = Vector2.Zero; + } + else if (!_moveFlag) + { + _moveFlag = true; + var pos = Master.GlobalPosition; + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - pos - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + _prevPos = pos; + } + else + { + var pos = Master.GlobalPosition; + var lastSlideCollision = Master.GetLastSlideCollision(); + if (lastSlideCollision != null && lastSlideCollision.GetCollider() is Role) //碰到其他角色 + { + _pauseTimer = Utils.RandomRangeFloat(0.1f, 0.5f); + _isMoveOver = true; + _moveFlag = false; + Master.BasisVelocity = Vector2.Zero; + } + else + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - pos - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + + if (_prevPos.DistanceSquaredTo(pos) <= 0.01f) + { + _lockTimer += delta; + } + else + { + _prevPos = pos; + } + } + } + + //关闭射线检测 + Master.TestViewRayCastOver(); + } + } + + //移动结束 + private void RunOver() + { + float angle; + if (_againstWall) + { + angle = Utils.RandomRangeFloat(_againstWallNormalAngle - Mathf.Pi * 0.5f, + _againstWallNormalAngle + Mathf.Pi * 0.5f); + } + else + { + angle = Utils.RandomRangeFloat(0, Mathf.Pi * 2f); + } + + var len = Utils.RandomRangeInt(30, 200); + _nextPos = new Vector2(len, 0).Rotated(angle) + Master.GlobalPosition; + //获取射线碰到的坐标 + if (Master.TestViewRayCast(_nextPos)) //碰到墙壁 + { + _nextPos = Master.ViewRay.GetCollisionPoint(); + _againstWall = true; + _againstWallNormalAngle = Master.ViewRay.GetCollisionNormal().Angle(); + } + else + { + _againstWall = false; + } + + Master.NavigationAgent2D.TargetPosition = _nextPos; + Master.LookTargetPosition(_nextPos); + } + + public override void DebugDraw() + { + Master.DrawLine(new Vector2(0, -8), Master.ToLocal(_nextPos), Colors.Green); + } +} diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiProbeState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiProbeState.cs new file mode 100644 index 0000000..1015095 --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiProbeState.cs @@ -0,0 +1,20 @@ + +/// <summary> +/// Ai 不确定玩家位置 +/// </summary> +public class AiProbeState : StateBase<Enemy, AiStateEnum> +{ + public AiProbeState() : base(AiStateEnum.AiProbe) + { + } + + public override void Process(float delta) + { + //其他敌人发现玩家 + if (Master.CanChangeLeaveFor()) + { + ChangeState(AiStateEnum.AiLeaveFor); + return; + } + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiSurroundState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiSurroundState.cs new file mode 100644 index 0000000..9a4c90e --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiSurroundState.cs @@ -0,0 +1,176 @@ + +using Godot; + +/// <summary> +/// 距离目标足够近, 在目标附近随机移动, 并开火 +/// </summary> +public class AiSurroundState : StateBase<Enemy, AiStateEnum> +{ + /// <summary> + /// 目标是否在视野内 + /// </summary> + public bool IsInView = true; + + //是否移动结束 + private bool _isMoveOver; + + //移动停顿计时器 + private float _pauseTimer; + private bool _moveFlag; + + //下一个移动点 + private Vector2 _nextPosition; + + //上一帧位置 + private Vector2 _prevPos; + //卡在一个位置的时间 + private float _lockTimer; + + public AiSurroundState() : base(AiStateEnum.AiSurround) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + IsInView = true; + _isMoveOver = true; + _pauseTimer = 0; + _moveFlag = false; + } + + public override void Process(float delta) + { + //先检查弹药是否打光 + if (Master.IsAllWeaponTotalAmmoEmpty()) + { + //再寻找是否有可用的武器 + var targetWeapon = Master.FindTargetWeapon(); + if (targetWeapon != null) + { + ChangeState(AiStateEnum.AiFindAmmo, targetWeapon); + return; + } + } + + var playerPos = Player.Current.GetCenterPosition(); + var weapon = Master.Holster.ActiveWeapon; + + //枪口指向玩家 + Master.LookTargetPosition(playerPos); + + //检测玩家是否在视野内 + if (Master.IsInTailAfterViewRange(playerPos)) + { + IsInView = !Master.TestViewRayCast(playerPos); + //关闭射线检测 + Master.TestViewRayCastOver(); + } + else + { + IsInView = false; + } + + if (IsInView) + { + if (_pauseTimer >= 0) + { + Master.AnimatedSprite.Play(AnimatorNames.Idle); + _pauseTimer -= delta; + } + else if (_isMoveOver) //移动已经完成 + { + RunOver(playerPos); + _isMoveOver = false; + } + else + { + if (_lockTimer >= 1) //卡在一个点超过一秒 + { + RunOver(playerPos); + _isMoveOver = false; + _lockTimer = 0; + } + else if (Master.NavigationAgent2D.IsNavigationFinished()) //到达终点 + { + _pauseTimer = Utils.RandomRangeFloat(0f, 0.5f); + _isMoveOver = true; + _moveFlag = false; + Master.BasisVelocity = Vector2.Zero; + } + else if (!_moveFlag) + { + _moveFlag = true; + //计算移动 + var pos = Master.GlobalPosition; + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + else + { + var pos = Master.GlobalPosition; + var lastSlideCollision = Master.GetLastSlideCollision(); + if (lastSlideCollision != null && lastSlideCollision.GetCollider() is Role) //碰到其他角色 + { + _pauseTimer = Utils.RandomRangeFloat(0f, 0.3f); + _isMoveOver = true; + _moveFlag = false; + Master.BasisVelocity = Vector2.Zero; + } + else + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - pos - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + + if (_prevPos.DistanceSquaredTo(pos) <= 0.01f) + { + _lockTimer += delta; + } + else + { + _prevPos = pos; + } + } + + if (weapon != null) + { + var position = Master.GlobalPosition; + if (position.DistanceSquaredTo(playerPos) > Mathf.Pow(Master.GetWeaponRange(0.7f), 2)) //玩家离开正常射击范围 + { + ChangeState(AiStateEnum.AiFollowUp); + } + else + { + //发起攻击 + Master.EnemyAttack(delta); + } + } + } + } + else //目标离开视野 + { + ChangeState(AiStateEnum.AiTailAfter); + } + } + + private void RunOver(Vector2 targetPos) + { + var weapon = Master.Holster.ActiveWeapon; + var distance = (int)(weapon == null ? 150 : (weapon.Attribute.BulletMinDistance * 0.7f)); + _nextPosition = new Vector2( + targetPos.X + Utils.RandomRangeInt(-distance, distance), + targetPos.Y + Utils.RandomRangeInt(-distance, distance) + ); + Master.NavigationAgent2D.TargetPosition = _nextPosition; + } + + public override void DebugDraw() + { + Master.DrawLine(new Vector2(0, -8), Master.ToLocal(_nextPosition), Colors.White); + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiTailAfterState.cs b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiTailAfterState.cs new file mode 100644 index 0000000..455e4ae --- /dev/null +++ b/DungeonShooting_Godot/src/game/activity/role/enemy/state/AiTailAfterState.cs @@ -0,0 +1,125 @@ + +using Godot; + +/// <summary> +/// AI 发现玩家, 跟随玩家 +/// </summary> +public class AiTailAfterState : StateBase<Enemy, AiStateEnum> +{ + /// <summary> + /// 目标是否在视野半径内 + /// </summary> + private bool _isInViewRange; + + //导航目标点刷新计时器 + private float _navigationUpdateTimer = 0; + private float _navigationInterval = 0.3f; + + //目标从视野消失时已经过去的时间 + private float _viewTimer; + + public AiTailAfterState() : base(AiStateEnum.AiTailAfter) + { + } + + public override void Enter(AiStateEnum prev, params object[] args) + { + _isInViewRange = true; + _navigationUpdateTimer = 0; + _viewTimer = 0; + + //先检查弹药是否打光 + if (Master.IsAllWeaponTotalAmmoEmpty()) + { + //再寻找是否有可用的武器 + var targetWeapon = Master.FindTargetWeapon(); + if (targetWeapon != null) + { + ChangeState(AiStateEnum.AiFindAmmo, targetWeapon); + } + } + } + + public override void Process(float delta) + { + //这个状态下不会有攻击事件, 所以没必要每一帧检查是否弹药耗尽 + + var playerPos = Player.Current.GetCenterPosition(); + + //更新玩家位置 + if (_navigationUpdateTimer <= 0) + { + //每隔一段时间秒更改目标位置 + _navigationUpdateTimer = _navigationInterval; + Master.NavigationAgent2D.TargetPosition = playerPos; + } + else + { + _navigationUpdateTimer -= delta; + } + + //枪口指向玩家 + Master.LookTargetPosition(playerPos); + + if (!Master.NavigationAgent2D.IsNavigationFinished()) + { + //计算移动 + var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); + Master.AnimatedSprite.Play(AnimatorNames.Run); + Master.BasisVelocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * + Master.RoleState.MoveSpeed; + } + else + { + Master.BasisVelocity = Vector2.Zero; + } + //检测玩家是否在视野内, 如果在, 则切换到 AiTargetInView 状态 + if (Master.IsInTailAfterViewRange(playerPos)) + { + if (!Master.TestViewRayCast(playerPos)) //看到玩家 + { + //关闭射线检测 + Master.TestViewRayCastOver(); + //切换成发现目标状态 + ChangeState(AiStateEnum.AiFollowUp); + return; + } + else + { + //关闭射线检测 + Master.TestViewRayCastOver(); + } + } + + //检测玩家是否在穿墙视野范围内, 直接检测距离即可 + _isInViewRange = Master.IsInViewRange(playerPos); + if (_isInViewRange) + { + _viewTimer = 0; + } + else //超出视野 + { + if (_viewTimer > 10) //10秒 + { + ChangeState(AiStateEnum.AiNormal); + } + else + { + _viewTimer += delta; + } + } + } + + public override void DebugDraw() + { + var playerPos = Player.Current.GetCenterPosition(); + if (_isInViewRange) + { + Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Orange); + } + else + { + Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Blue); + } + } +} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/activity/weapon/Weapon.cs b/DungeonShooting_Godot/src/game/activity/weapon/Weapon.cs index 4cae2a7..a2541d1 100644 --- a/DungeonShooting_Godot/src/game/activity/weapon/Weapon.cs +++ b/DungeonShooting_Godot/src/game/activity/weapon/Weapon.cs @@ -246,7 +246,7 @@ public override void OnInit() { - InitWeapon(_GetWeaponAttribute(ItemId)); + InitWeapon(_GetWeaponAttribute(ItemConfig.Id)); AnimatedSprite.AnimationFinished += OnAnimationFinished; } @@ -269,7 +269,7 @@ if (Attribute.AmmoCapacity > Attribute.MaxAmmoCapacity) { Attribute.AmmoCapacity = Attribute.MaxAmmoCapacity; - GD.PrintErr("弹夹的容量不能超过弹药上限, 武器id: " + ItemId); + GD.PrintErr("弹夹的容量不能超过弹药上限, 武器id: " + ItemConfig.Id); } //弹药量 CurrAmmo = Attribute.AmmoCapacity; @@ -366,7 +366,8 @@ /// <summary> /// 当武器从武器袋中移除时调用 /// </summary> - protected virtual void OnRemove() + /// <param name="master">移除该武器的角色</param> + protected virtual void OnRemove(Role master) { } @@ -1387,7 +1388,7 @@ { var masterWeapon = roleMaster.Holster.ActiveWeapon; //查找是否有同类型武器 - var index = roleMaster.Holster.FindWeapon(ItemId); + var index = roleMaster.Holster.FindWeapon(ItemConfig.Id); if (index != -1) //如果有这个武器 { if (CurrAmmo + ResidueAmmo != 0) //子弹不为空 @@ -1434,7 +1435,7 @@ { var holster = roleMaster.Holster; //查找是否有同类型武器 - var index = holster.FindWeapon(ItemId); + var index = holster.FindWeapon(ItemConfig.Id); if (index != -1) //如果有这个武器 { if (CurrAmmo + ResidueAmmo == 0) //没有子弹了 @@ -1582,13 +1583,14 @@ /// </summary> public void RemoveAt() { + var master = Master; Master = null; CollisionLayer = _tempLayer; _weaponAttribute = _playerWeaponAttribute; AnimatedSprite.Position = _tempAnimatedSpritePosition; //清除 Ai 拾起标记 RemoveSign(SignNames.AiFindWeaponSign); - OnRemove(); + OnRemove(master); } /// <summary> diff --git a/DungeonShooting_Godot/src/game/buff/RoleState.cs b/DungeonShooting_Godot/src/game/buff/RoleState.cs deleted file mode 100644 index 53bcff3..0000000 --- a/DungeonShooting_Godot/src/game/buff/RoleState.cs +++ /dev/null @@ -1,11 +0,0 @@ - -/// <summary> -/// 角色属性类 -/// </summary> -public class RoleState -{ - /// <summary> - /// 移动速度 - /// </summary> - public float MoveSpeed; -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/manager/ActivityIdPrefix.cs b/DungeonShooting_Godot/src/game/manager/ActivityIdPrefix.cs index c2edfc4..6c3d949 100644 --- a/DungeonShooting_Godot/src/game/manager/ActivityIdPrefix.cs +++ b/DungeonShooting_Godot/src/game/manager/ActivityIdPrefix.cs @@ -41,9 +41,13 @@ /// </summary> Effect, /// <summary> + /// 道具 + /// </summary> + Prop, + /// <summary> /// 其它类型 /// </summary> - Other, + Other = 99, } /// <summary> @@ -75,6 +79,10 @@ /// </summary> public const string Effect = "effect"; /// <summary> + /// 道具 + /// </summary> + public const string Prop = "prop"; + /// <summary> /// 其他类型 /// </summary> public const string Other = "other"; @@ -103,6 +111,8 @@ return Shell; case ActivityPrefixType.Effect: return Effect; + case ActivityPrefixType.Prop: + return Prop; case ActivityPrefixType.Other: return Other; } diff --git a/DungeonShooting_Godot/src/game/role/CampEnum.cs b/DungeonShooting_Godot/src/game/role/CampEnum.cs deleted file mode 100644 index 6954070..0000000 --- a/DungeonShooting_Godot/src/game/role/CampEnum.cs +++ /dev/null @@ -1,10 +0,0 @@ - -public enum CampEnum -{ - // 阵营1, 玩家 - Camp1, - // 阵营2, 敌人 - Camp2, - // 阵营3, 中立单位 - Camp3 -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/Characters/CPlusPlus.cs b/DungeonShooting_Godot/src/game/role/Characters/CPlusPlus.cs deleted file mode 100644 index 00e5353..0000000 --- a/DungeonShooting_Godot/src/game/role/Characters/CPlusPlus.cs +++ /dev/null @@ -1,36 +0,0 @@ -#region 设计思路 -// c/c++ -// 被动:易于精通 随着刷的房间的增多 提升对于 道具的加成 增加攻击力 或者 增强对应效果 -// -// 速度:中偏上 -// 血量:中 -// 护盾:下 -// -// 专属武器:指针 近战武器 -// 武器描述:短按 向目标方向 刺出 对路径中的 敌人或可破坏建筑 造成伤害 -// 长按 1.8 秒 向目标方向 冲刺 并消除 途中的弹幕 蓄力过程会被打断 打断后强制取消攻击 -// -// -// 每个角色都应该有对应的被动 属性 专属武器 -#endregion - -public partial class CPlusPlus : Player -{ - - - public override void OnInit() - { - base.OnInit(); - #region 初始属性 - - MaxHp = 55; - MoveSpeed = 130f; - #endregion - } - - // public CPlusPlus() : base(ResourcePath.prefab_role_CPlusPlus_tscn) - // { - - // } - -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/FaceDirection.cs b/DungeonShooting_Godot/src/game/role/FaceDirection.cs deleted file mode 100644 index 5be28ef..0000000 --- a/DungeonShooting_Godot/src/game/role/FaceDirection.cs +++ /dev/null @@ -1,8 +0,0 @@ -/// <summary> -/// 脸的朝向 -/// </summary> -public enum FaceDirection -{ - Left = -1, - Right = 1, -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/MountRotation.cs b/DungeonShooting_Godot/src/game/role/MountRotation.cs deleted file mode 100644 index e265b10..0000000 --- a/DungeonShooting_Godot/src/game/role/MountRotation.cs +++ /dev/null @@ -1,67 +0,0 @@ - -using Godot; - -/// <summary> -/// 用于限定 Marker2D 节点的旋转角度 -/// </summary> -[Tool] -public partial class MountRotation : Marker2D -{ - /// <summary> - /// 吸附角度 - /// </summary> - private int _adsorption = 6; - - /// <summary> - /// 所在的角色 - /// </summary> - public Role Master { get; set; } - - /// <summary> - /// 当前节点真实的旋转角度, 角度制 - /// </summary> - public float RealRotationDegrees { get; private set; } - - /// <summary> - /// 当前节点真实的旋转角度, 弧度制 - /// </summary> - public float RealRotation => Mathf.DegToRad(RealRotationDegrees); - - /// <summary> - /// 设置看向的目标点 - /// </summary> - public void SetLookAt(Vector2 target) - { - var myPos = GlobalPosition; - var angle = Mathf.RadToDeg((target - myPos).Angle()); - - if (Master.Face == FaceDirection.Left) - { - if (angle < 0 && angle > -80) - { - angle = -80; - } - else if (angle >= 0 && angle < 80) - { - angle = 80; - } - } - else - { - angle = Mathf.Clamp(angle, -100, 100); - } - - RealRotationDegrees = angle; - - // if (Master.GlobalPosition.X >= target.X) - // { - // angle = -angle; - // } - GlobalRotationDegrees = AdsorptionAngle(angle); - } - - private float AdsorptionAngle(float angle) - { - return Mathf.Round(angle / _adsorption) * _adsorption; - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/Player.cs b/DungeonShooting_Godot/src/game/role/Player.cs deleted file mode 100644 index cb58fc9..0000000 --- a/DungeonShooting_Godot/src/game/role/Player.cs +++ /dev/null @@ -1,249 +0,0 @@ -using Godot; - - -/// <summary> -/// 玩家角色基类, 所有角色都必须继承该类 -/// </summary> -[Tool, GlobalClass] -public partial class Player : Role -{ - /// <summary> - /// 获取当前操作的角色 - /// </summary> - public static Player Current { get; private set; } - - /// <summary> - /// 移动加速度 - /// </summary> - public float Acceleration { get; set; } = 1500f; - - /// <summary> - /// 移动摩擦力 - /// </summary> - public float Friction { get; set; } = 800f; - - /// <summary> - /// 设置当前操作的玩家对象 - /// </summary> - public static void SetCurrentPlayer(Player player) - { - Current = player; - //设置相机和鼠标跟随玩家 - GameCamera.Main.SetFollowTarget(player); - GameApplication.Instance.Cursor.SetMountRole(player); - } - - public override void OnInit() - { - base.OnInit(); - - AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Props | PhysicsLayer.Enemy; - Camp = CampEnum.Camp1; - - //让相机跟随玩家 - // var remoteTransform = new RemoteTransform2D(); - // AddChild(remoteTransform); - // MainCamera.Main.GlobalPosition = GlobalPosition; - // MainCamera.Main.ResetSmoothing(); - // remoteTransform.RemotePath = remoteTransform.GetPathTo(MainCamera.Main); - - MaxHp = 6; - Hp = 6; - MaxShield = 2; - Shield = 2; - - // debug用 - // Acceleration = 3000; - // Friction = 3000; - // MoveSpeed = 500; - // CollisionLayer = 0; - // CollisionMask = 0; - // GameCamera.Main.Zoom = new Vector2(0.5f, 0.5f); - } - - protected override void Process(float delta) - { - if (IsDie) - { - return; - } - base.Process(delta); - //脸的朝向 - if (LookTarget == null) - { - var gPos = GlobalPosition; - Vector2 mousePos = InputManager.CursorPosition; - if (mousePos.X > gPos.X && Face == FaceDirection.Left) - { - Face = FaceDirection.Right; - } - else if (mousePos.X < gPos.X && Face == FaceDirection.Right) - { - Face = FaceDirection.Left; - } - //枪口跟随鼠标 - MountPoint.SetLookAt(mousePos); - } - - if (InputManager.Exchange) //切换武器 - { - ExchangeNext(); - } - else if (InputManager.Throw) //扔掉武器 - { - ThrowWeapon(); - - // //测试用的, 所有敌人也扔掉武器 - // if (Affiliation != null) - // { - // var enemies = Affiliation.FindIncludeItems(o => - // { - // return o.CollisionWithMask(PhysicsLayer.Enemy); - // }); - // foreach (var activityObject in enemies) - // { - // if (activityObject is Enemy enemy) - // { - // enemy.ThrowWeapon(); - // } - // } - // } - } - else if (InputManager.Interactive) //互动物体 - { - TriggerInteractive(); - } - else if (InputManager.Reload) //换弹 - { - Reload(); - } - if (InputManager.Fire) //开火 - { - Attack(); - } - - if (Input.IsKeyPressed(Key.P)) - { - Hurt(1000, 0); - } - } - - protected override void PhysicsProcess(float delta) - { - if (IsDie) - { - return; - } - - base.PhysicsProcess(delta); - HandleMoveInput(delta); - //播放动画 - PlayAnim(); - } - - protected override int OnHandlerHurt(int damage) - { - //修改受到的伤害, 每次只受到1点伤害 - return 1; - } - - protected override void OnHit(int damage, bool realHarm) - { - //进入无敌状态 - if (realHarm) //真实伤害 - { - PlayInvincibleFlashing(1.5f); - } - else //护盾抵消掉的 - { - PlayInvincibleFlashing(0.5f); - } - } - - protected override void OnChangeHp(int hp) - { - //GameApplication.Instance.Ui.SetHp(hp); - EventManager.EmitEvent(EventEnum.OnPlayerHpChange, hp); - } - - protected override void OnChangeMaxHp(int maxHp) - { - //GameApplication.Instance.Ui.SetMaxHp(maxHp); - EventManager.EmitEvent(EventEnum.OnPlayerMaxHpChange, maxHp); - } - - protected override void ChangeInteractiveItem(CheckInteractiveResult result) - { - //派发互动对象改变事件 - EventManager.EmitEvent(EventEnum.OnPlayerChangeInteractiveItem, result); - } - - protected override void OnChangeShield(int shield) - { - //GameApplication.Instance.Ui.SetShield(shield); - EventManager.EmitEvent(EventEnum.OnPlayerShieldChange, shield); - } - - protected override void OnChangeMaxShield(int maxShield) - { - //GameApplication.Instance.Ui.SetMaxShield(maxShield); - EventManager.EmitEvent(EventEnum.OnPlayerMaxShieldChange, maxShield); - } - - protected override void OnDie() - { - GameCamera.Main.SetFollowTarget(null); - BasisVelocity = Vector2.Zero; - MoveController.ClearForce(); - UiManager.Open_Settlement(); - //GameApplication.Instance.World.ProcessMode = ProcessModeEnum.WhenPaused; - } - - //处理角色移动的输入 - private void HandleMoveInput(float delta) - { - //角色移动 - Vector2 dir = InputManager.MoveAxis; - // 移动. 如果移动的数值接近0(是用 摇杆可能出现 方向 可能会出现浮点),就friction的值 插值 到 0 - // 如果 有输入 就以当前速度,用acceleration 插值到 对应方向 * 最大速度 - if (Mathf.IsZeroApprox(dir.X)) - { - BasisVelocity = new Vector2(Mathf.MoveToward(BasisVelocity.X, 0, Friction * delta), BasisVelocity.Y); - } - else - { - BasisVelocity = new Vector2(Mathf.MoveToward(BasisVelocity.X, dir.X * MoveSpeed, Acceleration * delta), - BasisVelocity.Y); - } - - if (Mathf.IsZeroApprox(dir.Y)) - { - BasisVelocity = new Vector2(BasisVelocity.X, Mathf.MoveToward(BasisVelocity.Y, 0, Friction * delta)); - } - else - { - BasisVelocity = new Vector2(BasisVelocity.X, - Mathf.MoveToward(BasisVelocity.Y, dir.Y * MoveSpeed, Acceleration * delta)); - } - } - - // 播放动画 - private void PlayAnim() - { - if (BasisVelocity != Vector2.Zero) - { - if ((Face == FaceDirection.Right && BasisVelocity.X >= 0) || Face == FaceDirection.Left && BasisVelocity.X <= 0) //向前走 - { - AnimatedSprite.Play(AnimatorNames.Run); - } - else if ((Face == FaceDirection.Right && BasisVelocity.X < 0) || Face == FaceDirection.Left && BasisVelocity.X > 0) //向后走 - { - AnimatedSprite.Play(AnimatorNames.ReverseRun); - } - } - else - { - AnimatedSprite.Play(AnimatorNames.Idle); - } - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/Role.cs b/DungeonShooting_Godot/src/game/role/Role.cs deleted file mode 100644 index 10ebd64..0000000 --- a/DungeonShooting_Godot/src/game/role/Role.cs +++ /dev/null @@ -1,737 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using Godot; - -/// <summary> -/// 角色基类 -/// </summary> -public abstract partial class Role : ActivityObject -{ - /// <summary> - /// 是否是 Ai - /// </summary> - public bool IsAi { get; protected set; } = false; - - /// <summary> - /// 默认攻击对象层级 - /// </summary> - public const uint DefaultAttackLayer = PhysicsLayer.Player | PhysicsLayer.Enemy | PhysicsLayer.Wall | PhysicsLayer.Props; - - /// <summary> - /// 伤害区域 - /// </summary> - [Export, ExportFillNode] - public Area2D HurtArea { get; set; } - - /// <summary> - /// 移动速度 - /// </summary> - public float MoveSpeed = 120f; - - /// <summary> - /// 所属阵营 - /// </summary> - public CampEnum Camp; - - /// <summary> - /// 攻击目标的碰撞器所属层级, 数据源自于: PhysicsLayer - /// </summary> - public uint AttackLayer { get; set; } = PhysicsLayer.Wall; - - // /// <summary> - // /// 携带的道具包裹 - // /// </summary> - // public List<object> PropsPack { get; } = new List<object>(); - - /// <summary> - /// 角色携带的武器袋 - /// </summary> - public Holster Holster { get; private set; } - - /// <summary> - /// 武器挂载点 - /// </summary> - [Export, ExportFillNode] - public MountRotation MountPoint { get; set; } - /// <summary> - /// 背后武器的挂载点 - /// </summary> - [Export, ExportFillNode] - public Marker2D BackMountPoint { get; set; } - - /// <summary> - /// 互动碰撞区域 - /// </summary> - [Export, ExportFillNode] - public Area2D InteractiveArea { get; set; } - - /// <summary> - /// 脸的朝向 - /// </summary> - public FaceDirection Face { get => _face; set => SetFace(value); } - private FaceDirection _face; - - /// <summary> - /// 是否死亡 - /// </summary> - public bool IsDie { get; private set; } - - /// <summary> - /// 血量 - /// </summary> - public int Hp - { - get => _hp; - protected set - { - int temp = _hp; - _hp = value; - if (temp != _hp) OnChangeHp(_hp); - } - } - private int _hp = 20; - - /// <summary> - /// 最大血量 - /// </summary> - public int MaxHp - { - get => _maxHp; - protected set - { - int temp = _maxHp; - _maxHp = value; - //护盾值改变 - if (temp != _maxHp) OnChangeMaxHp(_maxHp); - } - } - private int _maxHp = 20; - - /// <summary> - /// 当前护盾值 - /// </summary> - public int Shield - { - get => _shield; - protected set - { - int temp = _shield; - _shield = value; - //护盾被破坏 - if (temp > 0 && _shield <= 0) OnShieldDestroy(); - //护盾值改变 - if (temp != _shield) OnChangeShield(_shield); - } - } - private int _shield = 0; - - /// <summary> - /// 最大护盾值 - /// </summary> - public int MaxShield - { - get => _maxShield; - protected set - { - int temp = _maxShield; - _maxShield = value; - if (temp != _maxShield) OnChangeMaxShield(_maxShield); - } - } - private int _maxShield = 0; - - /// <summary> - /// 单格护盾恢复时间 - /// </summary> - private float ShieldRecoveryTime { get; set; } = 8; - - /// <summary> - /// 无敌状态 - /// </summary> - public bool Invincible - { - get => _invincible; - set - { - if (_invincible != value) - { - if (value) //无敌状态 - { - HurtArea.CollisionLayer = _currentLayer; - _flashingInvincibleTimer = -1; - _flashingInvincibleFlag = false; - } - else //正常状态 - { - HurtArea.CollisionLayer = _currentLayer; - SetBlendAlpha(1); - } - } - - _invincible = value; - } - } - - private bool _invincible = false; - - /// <summary> - /// 当前角色所看向的对象, 也就是枪口指向的对象 - /// </summary> - public ActivityObject LookTarget { get; set; } - - //初始缩放 - private Vector2 _startScale; - //所有角色碰撞的道具 - private readonly List<ActivityObject> _interactiveItemList = new List<ActivityObject>(); - - private CheckInteractiveResult _tempResultData; - private uint _currentLayer; - //闪烁计时器 - private float _flashingInvincibleTimer = -1; - //闪烁状态 - private bool _flashingInvincibleFlag = false; - //闪烁动画协程id - private long _invincibleFlashingId = -1; - //护盾恢复计时器 - private float _shieldRecoveryTimer = 0; - - /// <summary> - /// 可以互动的道具 - /// </summary> - public ActivityObject InteractiveItem { get; private set; } - - /// <summary> - /// 当血量改变时调用 - /// </summary> - protected virtual void OnChangeHp(int hp) - { - } - - /// <summary> - /// 当最大血量改变时调用 - /// </summary> - protected virtual void OnChangeMaxHp(int maxHp) - { - } - - /// <summary> - /// 护盾值改变时调用 - /// </summary> - protected virtual void OnChangeShield(int shield) - { - } - - /// <summary> - /// 最大护盾值改变时调用 - /// </summary> - protected virtual void OnChangeMaxShield(int maxShield) - { - } - - /// <summary> - /// 当护盾被破坏时调用 - /// </summary> - protected virtual void OnShieldDestroy() - { - } - - /// <summary> - /// 当受伤时调用 - /// </summary> - /// <param name="damage">受到的伤害</param> - /// <param name="realHarm">是否受到真实伤害, 如果为false, 则表示该伤害被互动格挡掉了</param> - protected virtual void OnHit(int damage, bool realHarm) - { - } - - /// <summary> - /// 受到伤害时调用, 用于改变受到的伤害值 - /// </summary> - /// <param name="damage">受到的伤害</param> - protected virtual int OnHandlerHurt(int damage) - { - return damage; - } - - /// <summary> - /// 当可互动的物体改变时调用, result 参数为 null 表示变为不可互动 - /// </summary> - /// <param name="result">检测是否可互动时的返回值</param> - protected virtual void ChangeInteractiveItem(CheckInteractiveResult result) - { - } - - /// <summary> - /// 死亡时调用 - /// </summary> - protected virtual void OnDie() - { - } - - public override void OnInit() - { - Holster = new Holster(this); - _startScale = Scale; - MountPoint.Master = this; - - HurtArea.CollisionLayer = CollisionLayer; - HurtArea.CollisionMask = 0; - _currentLayer = HurtArea.CollisionLayer; - - Face = FaceDirection.Right; - - //连接互动物体信号 - InteractiveArea.BodyEntered += _OnPropsEnter; - InteractiveArea.BodyExited += _OnPropsExit; - } - - protected override void Process(float delta) - { - //看向目标 - if (LookTarget != null) - { - Vector2 pos = LookTarget.GlobalPosition; - //脸的朝向 - var gPos = GlobalPosition; - if (pos.X > gPos.X && Face == FaceDirection.Left) - { - Face = FaceDirection.Right; - } - else if (pos.X < gPos.X && Face == FaceDirection.Right) - { - Face = FaceDirection.Left; - } - //枪口跟随目标 - MountPoint.SetLookAt(pos); - } - - //检查可互动的道具 - bool findFlag = false; - for (int i = 0; i < _interactiveItemList.Count; i++) - { - var item = _interactiveItemList[i]; - if (item == null || item.IsDestroyed) - { - _interactiveItemList.RemoveAt(i--); - } - else - { - //找到可互动的道具了 - if (!findFlag) - { - var result = item.CheckInteractive(this); - if (result.CanInteractive) //可以互动 - { - findFlag = true; - if (InteractiveItem != item) //更改互动物体 - { - InteractiveItem = item; - ChangeInteractiveItem(result); - } - else if (result.ShowIcon != _tempResultData.ShowIcon) //切换状态 - { - ChangeInteractiveItem(result); - } - } - _tempResultData = result; - } - } - } - //没有可互动的道具 - if (!findFlag && InteractiveItem != null) - { - InteractiveItem = null; - ChangeInteractiveItem(null); - } - - //无敌状态, 播放闪烁动画 - if (Invincible) - { - _flashingInvincibleTimer -= delta; - if (_flashingInvincibleTimer <= 0) - { - _flashingInvincibleTimer = 0.15f; - if (_flashingInvincibleFlag) - { - _flashingInvincibleFlag = false; - SetBlendAlpha(0.7f); - } - else - { - _flashingInvincibleFlag = true; - SetBlendAlpha(0); - } - } - - _shieldRecoveryTimer = 0; - } - else //恢复护盾 - { - if (Shield < MaxShield) - { - _shieldRecoveryTimer += delta; - if (_shieldRecoveryTimer >= ShieldRecoveryTime) //时间到, 恢复 - { - Shield++; - _shieldRecoveryTimer = 0; - } - } - else - { - _shieldRecoveryTimer = 0; - } - } - } - - /// <summary> - /// 当武器放到后背时调用, 用于设置武器位置和角度 - /// </summary> - /// <param name="weapon">武器实例</param> - /// <param name="index">放入武器袋的位置</param> - public virtual void OnPutBackMount(Weapon weapon, int index) - { - if (index < 8) - { - if (index % 2 == 0) - { - weapon.Position = new Vector2(-4, 3); - weapon.RotationDegrees = 90 - (index / 2f) * 20; - weapon.Scale = new Vector2(-1, 1); - } - else - { - weapon.Position = new Vector2(4, 3); - weapon.RotationDegrees = 270 + (index - 1) / 2f * 20; - weapon.Scale = new Vector2(1, 1); - } - } - else - { - weapon.Visible = false; - } - } - - protected override void OnAffiliationChange() - { - //身上的武器的所属区域也得跟着变 - Holster.ForEach((weapon, i) => - { - if (AffiliationArea != null) - { - AffiliationArea.InsertItem(weapon); - } - else if (weapon.AffiliationArea != null) - { - weapon.AffiliationArea.RemoveItem(weapon); - } - }); - } - - /// <summary> - /// 获取当前角色的中心点坐标 - /// </summary> - public Vector2 GetCenterPosition() - { - return MountPoint.GlobalPosition; - } - - /// <summary> - /// 使角色看向指定的坐标, - /// 注意, 调用该函数会清空 LookTarget, 因为拥有 LookTarget 时也会每帧更新玩家视野位置 - /// </summary> - /// <param name="pos"></param> - public void LookTargetPosition(Vector2 pos) - { - LookTarget = null; - //脸的朝向 - var gPos = GlobalPosition; - if (pos.X > gPos.X && Face == FaceDirection.Left) - { - Face = FaceDirection.Right; - } - else if (pos.X < gPos.X && Face == FaceDirection.Right) - { - Face = FaceDirection.Left; - } - //枪口跟随目标 - MountPoint.SetLookAt(pos); - } - - /// <summary> - /// 判断指定坐标是否在角色视野方向 - /// </summary> - public bool IsPositionInForward(Vector2 pos) - { - var gps = GlobalPosition; - return (Face == FaceDirection.Left && pos.X <= gps.X) || - (Face == FaceDirection.Right && pos.X >= gps.X); - } - - /// <summary> - /// 返回所有武器是否弹药都打光了 - /// </summary> - public bool IsAllWeaponTotalAmmoEmpty() - { - foreach (var weapon in Holster.Weapons) - { - if (weapon != null && !weapon.IsTotalAmmoEmpty()) - { - return false; - } - } - - return true; - } - - /// <summary> - /// 拾起一个武器, 返回是否成功拾取, 如果不想立刻切换到该武器, exchange 请传 false - /// </summary> - /// <param name="weapon">武器对象</param> - /// <param name="exchange">是否立即切换到该武器, 默认 true </param> - public virtual bool PickUpWeapon(Weapon weapon, bool exchange = true) - { - if (Holster.PickupWeapon(weapon, exchange) != -1) - { - //从可互动队列中移除 - _interactiveItemList.Remove(weapon); - return true; - } - - return false; - } - - /// <summary> - /// 切换到下一个武器 - /// </summary> - public virtual void ExchangeNext() - { - Holster.ExchangeNext(); - } - - /// <summary> - /// 切换到上一个武器 - /// </summary> - public virtual void ExchangePrev() - { - Holster.ExchangePrev(); - } - - /// <summary> - /// 扔掉当前使用的武器, 切换到上一个武器 - /// </summary> - public virtual void ThrowWeapon() - { - ThrowWeapon(Holster.ActiveIndex); - } - - /// <summary> - /// 扔掉指定位置的武器 - /// </summary> - /// <param name="index">武器在武器袋中的位置</param> - public virtual void ThrowWeapon(int index) - { - var weapon = Holster.GetWeapon(index); - if (weapon == null) - { - return; - } - - var temp = weapon.AnimatedSprite.Position; - if (Face == FaceDirection.Left) - { - temp.Y = -temp.Y; - } - //var pos = GlobalPosition + temp.Rotated(weapon.GlobalRotation); - Holster.RemoveWeapon(index); - //播放抛出效果 - weapon.ThrowWeapon(this, GlobalPosition); - } - - /// <summary> - /// 返回是否存在可互动的物体 - /// </summary> - public bool HasInteractive() - { - return InteractiveItem != null; - } - - /// <summary> - /// 触发与碰撞的物体互动, 并返回与其互动的物体 - /// </summary> - public ActivityObject TriggerInteractive() - { - if (HasInteractive()) - { - var item = InteractiveItem; - item.Interactive(this); - return item; - } - - return null; - } - - /// <summary> - /// 触发换弹 - /// </summary> - public virtual void Reload() - { - if (Holster.ActiveWeapon != null) - { - Holster.ActiveWeapon.Reload(); - } - } - - /// <summary> - /// 触发攻击 - /// </summary> - public virtual void Attack() - { - if (Holster.ActiveWeapon != null) - { - Holster.ActiveWeapon.Trigger(); - } - } - - /// <summary> - /// 受到伤害, 如果是在碰撞信号处理函数中调用该函数, 请使用 CallDeferred 来延时调用, 否则很有可能导致报错 - /// </summary> - /// <param name="damage">伤害的量</param> - /// <param name="angle">角度</param> - public virtual void Hurt(int damage, float angle) - { - //受伤闪烁, 无敌状态 - if (Invincible) - { - return; - } - - //计算真正受到的伤害 - damage = OnHandlerHurt(damage); - if (damage <= 0) - { - return; - } - - var flag = Shield > 0; - if (flag) - { - Shield -= damage; - } - else - { - Hp -= damage; - //播放血液效果 - // var packedScene = ResourceManager.Load<PackedScene>(ResourcePath.prefab_effect_Blood_tscn); - // var blood = packedScene.Instance<Blood>(); - // blood.GlobalPosition = GlobalPosition; - // blood.Rotation = angle; - // GameApplication.Instance.Node3D.GetRoot().AddChild(blood); - } - OnHit(damage, !flag); - - //受伤特效 - PlayHitAnimation(); - - //死亡判定 - if (Hp <= 0) - { - //死亡 - if (!IsDie) - { - IsDie = true; - OnDie(); - } - } - } - - /// <summary> - /// 播放无敌状态闪烁动画 - /// </summary> - /// <param name="time">持续时间</param> - public void PlayInvincibleFlashing(float time) - { - Invincible = true; - if (_invincibleFlashingId >= 0) //上一个还没结束 - { - StopCoroutine(_invincibleFlashingId); - } - - _invincibleFlashingId = StartCoroutine(RunInvincibleFlashing(time)); - } - - /// <summary> - /// 停止无敌状态闪烁动画 - /// </summary> - public void StopInvincibleFlashing() - { - Invincible = false; - if (_invincibleFlashingId >= 0) - { - StopCoroutine(_invincibleFlashingId); - _invincibleFlashingId = -1; - } - } - - private IEnumerator RunInvincibleFlashing(float time) - { - yield return new WaitForSeconds(time); - _invincibleFlashingId = -1; - Invincible = false; - } - - /// <summary> - /// 设置脸的朝向 - /// </summary> - private void SetFace(FaceDirection face) - { - if (_face != face) - { - _face = face; - if (face == FaceDirection.Right) - { - RotationDegrees = 0; - Scale = _startScale; - } - else - { - RotationDegrees = 180; - Scale = new Vector2(_startScale.X, -_startScale.Y); - } - } - } - - /// <summary> - /// 连接信号: InteractiveArea.BodyEntered - /// 与物体碰撞 - /// </summary> - private void _OnPropsEnter(Node2D other) - { - if (other is ActivityObject propObject && !propObject.CollisionWithMask(PhysicsLayer.OnHand)) - { - if (!_interactiveItemList.Contains(propObject)) - { - _interactiveItemList.Add(propObject); - } - } - } - - /// <summary> - /// 连接信号: InteractiveArea.BodyExited - /// 物体离开碰撞区域 - /// </summary> - private void _OnPropsExit(Node2D other) - { - if (other is ActivityObject propObject) - { - if (_interactiveItemList.Contains(propObject)) - { - _interactiveItemList.Remove(propObject); - } - if (InteractiveItem == propObject) - { - InteractiveItem = null; - ChangeInteractiveItem(null); - } - } - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs b/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs deleted file mode 100644 index c28da7e..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs +++ /dev/null @@ -1,459 +0,0 @@ -#region 基础敌人设计思路 -/* -敌人有三种状态: -状态1: 未发现玩家, 视野不可穿墙, 该状态下敌人移动比较规律, 移动速度较慢, 一旦玩家进入视野或者听到玩家枪声, 立刻切换至状态3, 该房间的敌人不能再回到状态1 -状态2: 发现有玩家, 但不知道在哪, 视野不可穿墙, 该情况下敌人移动速度明显加快, 移动不规律, 一旦玩家进入视野或者听到玩家枪声, 立刻切换至状态3 -状态3: 明确知道玩家的位置, 视野允许穿墙, 移动速度与状态2一致, 进入该状态时, 敌人之间会相互告知玩家所在位置, 并朝着玩家位置开火, - 如果有墙格挡, 则有一定概率继续开火, 一旦玩家立刻敌人视野超哥一段时间, 敌人自动切换为状态2 - -敌人状态1只存在于少数房间内, 比如特殊房间, 大部分情况下敌人应该是状态2, 或者玩家进入房间时就被敌人发现 -*/ -#endregion - - -using Godot; - -/// <summary> -/// 基础敌人 -/// </summary> -[Tool, GlobalClass] -public partial class Enemy : Role -{ - /// <summary> - /// 敌人身上的状态机控制器 - /// </summary> - public StateController<Enemy, AiStateEnum> StateController { get; private set; } - - /// <summary> - /// 视野半径, 单位像素, 发现玩家后改视野范围可以穿墙 - /// </summary> - public float ViewRange { get; set; } = 250; - - /// <summary> - /// 发现玩家后的视野半径 - /// </summary> - public float TailAfterViewRange { get; set; } = 400; - - /// <summary> - /// 背后的视野半径, 单位像素 - /// </summary> - public float BackViewRange { get; set; } = 50; - - /// <summary> - /// 视野检测射线, 朝玩家打射线, 检测是否碰到墙 - /// </summary> - public RayCast2D ViewRay { get; private set; } - - /// <summary> - /// 导航代理 - /// </summary> - public NavigationAgent2D NavigationAgent2D { get; private set; } - - /// <summary> - /// 导航代理中点 - /// </summary> - public Marker2D NavigationPoint { get; private set; } - - //开火间隙时间 - private float _enemyAttackTimer = 0; - //目标在视野内的时间 - private float _targetInViewTime = 0; - - public override void OnInit() - { - base.OnInit(); - IsAi = true; - StateController = AddComponent<StateController<Enemy, AiStateEnum>>(); - - AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Props | PhysicsLayer.Player; - Camp = CampEnum.Camp2; - - MoveSpeed = 20; - - MaxHp = 20; - Hp = 20; - - //视野射线 - ViewRay = GetNode<RayCast2D>("ViewRay"); - NavigationPoint = GetNode<Marker2D>("NavigationPoint"); - NavigationAgent2D = NavigationPoint.GetNode<NavigationAgent2D>("NavigationAgent2D"); - - //PathSign = new PathSign(this, PathSignLength, GameApplication.Instance.Node3D.Player); - - //注册Ai状态机 - StateController.Register(new AiNormalState()); - StateController.Register(new AiProbeState()); - StateController.Register(new AiTailAfterState()); - StateController.Register(new AiFollowUpState()); - StateController.Register(new AiLeaveForState()); - StateController.Register(new AiSurroundState()); - StateController.Register(new AiFindAmmoState()); - - //默认状态 - StateController.ChangeStateInstant(AiStateEnum.AiNormal); - } - - public override void EnterTree() - { - base.EnterTree(); - if (!World.Enemy_InstanceList.Contains(this)) - { - World.Enemy_InstanceList.Add(this); - } - } - - public override void ExitTree() - { - base.ExitTree(); - World.Enemy_InstanceList.Remove(this); - } - - protected override void OnDie() - { - //扔掉所有武器 - var weapons = Holster.GetAndClearWeapon(); - for (var i = 0; i < weapons.Length; i++) - { - weapons[i].ThrowWeapon(this); - } - - var effPos = Position + new Vector2(0, -Altitude); - //血液特效 - var blood = ResourceManager.LoadAndInstantiate<AutoDestroyEffect>(ResourcePath.prefab_effect_activityObject_EnemyBloodEffect_tscn); - blood.Position = effPos - new Vector2(0, 12); - blood.AddToActivityRoot(RoomLayerEnum.NormalLayer); - - //创建敌人碎片 - var count = Utils.RandomRangeInt(3, 6); - for (var i = 0; i < count; i++) - { - var debris = Create(Ids.Id_effect0001); - debris.PutDown(effPos, RoomLayerEnum.NormalLayer); - debris.InheritVelocity(this); - } - - //派发敌人死亡信号 - EventManager.EmitEvent(EventEnum.OnEnemyDie, this); - Destroy(); - } - - protected override void Process(float delta) - { - base.Process(delta); - _enemyAttackTimer -= delta; - - //目标在视野内的时间 - var currState = StateController.CurrState; - if (currState == AiStateEnum.AiSurround || currState == AiStateEnum.AiFollowUp) - { - _targetInViewTime += delta; - } - else - { - _targetInViewTime = 0; - } - - EnemyPickUpWeapon(); - } - - protected override void OnHit(int damage, bool realHarm) - { - //受到伤害 - var state = StateController.CurrState; - if (state == AiStateEnum.AiNormal || state == AiStateEnum.AiProbe || state == AiStateEnum.AiLeaveFor) - { - StateController.ChangeState(AiStateEnum.AiTailAfter); - } - } - - /// <summary> - /// 返回地上的武器是否有可以拾取的, 也包含没有被其他敌人标记的武器 - /// </summary> - public bool CheckUsableWeaponInUnclaimed() - { - foreach (var unclaimedWeapon in World.Weapon_UnclaimedWeapons) - { - //判断是否能拾起武器, 条件: 相同的房间 - if (unclaimedWeapon.AffiliationArea == AffiliationArea) - { - if (!unclaimedWeapon.IsTotalAmmoEmpty()) - { - if (!unclaimedWeapon.HasSign(SignNames.AiFindWeaponSign)) - { - return true; - } - else - { - //判断是否可以移除该标记 - var enemy = unclaimedWeapon.GetSign<Enemy>(SignNames.AiFindWeaponSign); - if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁 - { - unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign); - return true; - } - else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了 - { - unclaimedWeapon.RemoveSign(SignNames.AiFindWeaponSign); - return true; - } - } - } - } - } - - return false; - } - - /// <summary> - /// 寻找可用的武器 - /// </summary> - public Weapon FindTargetWeapon() - { - Weapon target = null; - var position = Position; - foreach (var weapon in World.Weapon_UnclaimedWeapons) - { - //判断是否能拾起武器, 条件: 相同的房间, 或者当前房间目前没有战斗, 或者不在战斗房间 - if (weapon.AffiliationArea == AffiliationArea) - { - //还有弹药 - if (!weapon.IsTotalAmmoEmpty()) - { - //查询是否有其他敌人标记要拾起该武器 - if (weapon.HasSign(SignNames.AiFindWeaponSign)) - { - var enemy = weapon.GetSign<Enemy>(SignNames.AiFindWeaponSign); - if (enemy == this) //就是自己标记的 - { - - } - else if (enemy == null || enemy.IsDestroyed) //标记当前武器的敌人已经被销毁 - { - weapon.RemoveSign(SignNames.AiFindWeaponSign); - } - else if (!enemy.IsAllWeaponTotalAmmoEmpty()) //标记当前武器的敌人已经有新的武器了 - { - weapon.RemoveSign(SignNames.AiFindWeaponSign); - } - else //放弃这把武器 - { - continue; - } - } - - if (target == null) //第一把武器 - { - target = weapon; - } - else if (target.Position.DistanceSquaredTo(position) > - weapon.Position.DistanceSquaredTo(position)) //距离更近 - { - target = weapon; - } - } - } - } - - return target; - } - - /// <summary> - /// 检查是否能切换到 AiStateEnum.AiLeaveFor 状态 - /// </summary> - /// <returns></returns> - public bool CanChangeLeaveFor() - { - if (!World.Enemy_IsFindTarget) - { - return false; - } - - var currState = StateController.CurrState; - if (currState == AiStateEnum.AiNormal || currState == AiStateEnum.AiProbe) - { - //判断是否在同一个房间内 - return World.Enemy_FindTargetAffiliationSet.Contains(AffiliationArea); - } - - return false; - } - - /// <summary> - /// Ai触发的攻击 - /// </summary> - public void EnemyAttack(float delta) - { - var weapon = Holster.ActiveWeapon; - if (weapon != null) - { - if (weapon.IsTotalAmmoEmpty()) //当前武器弹药打空 - { - //切换到有子弹的武器 - var index = Holster.FindWeapon((we, i) => !we.IsTotalAmmoEmpty()); - if (index != -1) - { - Holster.ExchangeByIndex(index); - } - else //所有子弹打光 - { - - } - } - else if (weapon.Reloading) //换弹中 - { - - } - else if (weapon.IsAmmoEmpty()) //弹夹已经打空 - { - Reload(); - } - else if (_targetInViewTime >= weapon.Attribute.AiTargetLockingTime) //正常射击 - { - if (weapon.GetDelayedAttackTime() > 0) - { - Attack(); - } - else - { - if (weapon.Attribute.ContinuousShoot) //连发 - { - Attack(); - } - else //单发 - { - if (_enemyAttackTimer <= 0) - { - _enemyAttackTimer = 60f / weapon.Attribute.StartFiringSpeed; - Attack(); - } - } - } - } - } - } - - /// <summary> - /// 获取武器攻击范围 (最大距离值与最小距离的中间值) - /// </summary> - /// <param name="weight">从最小到最大距离的过渡量, 0 - 1, 默认 0.5</param> - public float GetWeaponRange(float weight = 0.5f) - { - if (Holster.ActiveWeapon != null) - { - var attribute = Holster.ActiveWeapon.Attribute; - return Mathf.Lerp(attribute.BulletMinDistance, attribute.BulletMaxDistance, weight); - } - - return 0; - } - - /// <summary> - /// 返回目标点是否在视野范围内 - /// </summary> - public bool IsInViewRange(Vector2 target) - { - var isForward = IsPositionInForward(target); - if (isForward) - { - if (GlobalPosition.DistanceSquaredTo(target) <= ViewRange * ViewRange) //没有超出视野半径 - { - return true; - } - } - - return false; - } - - /// <summary> - /// 返回目标点是否在跟随状态下的视野半径内 - /// </summary> - public bool IsInTailAfterViewRange(Vector2 target) - { - var isForward = IsPositionInForward(target); - if (isForward) - { - if (GlobalPosition.DistanceSquaredTo(target) <= TailAfterViewRange * TailAfterViewRange) //没有超出视野半径 - { - return true; - } - } - - return false; - } - - /// <summary> - /// 调用视野检测, 如果被墙壁和其它物体遮挡, 则返回被挡住视野的物体对象, 视野无阻则返回 null - /// </summary> - public bool TestViewRayCast(Vector2 target) - { - ViewRay.Enabled = true; - ViewRay.TargetPosition = ViewRay.ToLocal(target); - ViewRay.ForceRaycastUpdate(); - return ViewRay.IsColliding(); - } - - /// <summary> - /// 调用视野检测完毕后, 需要调用 TestViewRayCastOver() 来关闭视野检测射线 - /// </summary> - public void TestViewRayCastOver() - { - ViewRay.Enabled = false; - } - - /// <summary> - /// AI 拾起武器操作 - /// </summary> - private void EnemyPickUpWeapon() - { - //这几个状态不需要主动拾起武器操作 - var state = StateController.CurrState; - if (state == AiStateEnum.AiNormal) - { - return; - } - - //拾起地上的武器 - if (InteractiveItem is Weapon weapon) - { - if (Holster.ActiveWeapon == null) //手上没有武器, 无论如何也要拾起 - { - TriggerInteractive(); - return; - } - - //没弹药了 - if (weapon.IsTotalAmmoEmpty()) - { - return; - } - - var index = Holster.FindWeapon((we, i) => we.ItemId == weapon.ItemId); - if (index != -1) //与武器袋中武器类型相同, 补充子弹 - { - if (!Holster.GetWeapon(index).IsAmmoFull()) - { - TriggerInteractive(); - } - - return; - } - - // var index2 = Holster.FindWeapon((we, i) => - // we.Attribute.WeightType == weapon.Attribute.WeightType && we.IsTotalAmmoEmpty()); - var index2 = Holster.FindWeapon((we, i) => we.IsTotalAmmoEmpty()); - if (index2 != -1) //扔掉没子弹的武器 - { - ThrowWeapon(index2); - TriggerInteractive(); - return; - } - - // if (Holster.HasVacancy()) //有空位, 拾起武器 - // { - // TriggerInteractive(); - // return; - // } - } - } - -} diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AIStateEnum.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AIStateEnum.cs deleted file mode 100644 index 6bb8db4..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AIStateEnum.cs +++ /dev/null @@ -1,32 +0,0 @@ - -public enum AiStateEnum -{ - /// <summary> - /// Ai 状态, 正常, 未发现目标 - /// </summary> - AiNormal, - /// <summary> - /// 发现目标, 但不知道在哪 - /// </summary> - AiProbe, - /// <summary> - /// 收到其他敌人通知, 前往发现目标的位置 - /// </summary> - AiLeaveFor, - /// <summary> - /// 发现目标, 目标不在视野内, 但是知道位置 - /// </summary> - AiTailAfter, - /// <summary> - /// 目标在视野内, 跟进目标, 如果距离在子弹有效射程内, 则开火 - /// </summary> - AiFollowUp, - /// <summary> - /// 距离足够近, 在目标附近随机移动 - /// </summary> - AiSurround, - /// <summary> - /// Ai 寻找弹药 - /// </summary> - AiFindAmmo, -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiFindAmmoState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiFindAmmoState.cs deleted file mode 100644 index 9cfb3c6..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiFindAmmoState.cs +++ /dev/null @@ -1,152 +0,0 @@ - -using Godot; - -/// <summary> -/// Ai 寻找弹药, 进入该状态需要在参数中传入目标武器对象 -/// </summary> -public class AiFindAmmoState : StateBase<Enemy, AiStateEnum> -{ - - private Weapon _target; - - //导航目标点刷新计时器 - private float _navigationUpdateTimer = 0; - private float _navigationInterval = 1f; - - private bool _isInTailAfterRange = false; - private float _tailAfterTimer = 0; - - public AiFindAmmoState() : base(AiStateEnum.AiFindAmmo) - { - } - - public override void Enter(AiStateEnum prev, params object[] args) - { - if (args.Length == 0) - { - GD.PrintErr("进入 AiStateEnum.AiFindAmmo 状态必须要把目标武器当成参数传过来"); - ChangeState(prev); - return; - } - - SetTargetWeapon((Weapon)args[0]); - _navigationUpdateTimer = 0; - _isInTailAfterRange = false; - _tailAfterTimer = 0; - - //标记武器 - _target.SetSign(SignNames.AiFindWeaponSign, Master); - } - - public override void Process(float delta) - { - if (!Master.IsAllWeaponTotalAmmoEmpty()) //已经有弹药了 - { - ChangeState(GetNextState()); - return; - } - - //更新目标位置 - if (_navigationUpdateTimer <= 0) - { - //每隔一段时间秒更改目标位置 - _navigationUpdateTimer = _navigationInterval; - var position = _target.GlobalPosition; - Master.NavigationAgent2D.TargetPosition = position; - } - else - { - _navigationUpdateTimer -= delta; - } - - var playerPos = Player.Current.GetCenterPosition(); - //枪口指向玩家 - Master.LookTargetPosition(playerPos); - - if (_target.IsDestroyed || _target.IsTotalAmmoEmpty()) //已经被销毁, 或者弹药已经被其他角色捡走 - { - //再去寻找其他武器 - SetTargetWeapon(Master.FindTargetWeapon()); - - if (_target == null) //也没有其他可用的武器了 - { - ChangeState(GetNextState()); - } - } - else if (_target.Master == Master) //已经被自己拾起 - { - ChangeState(GetNextState()); - } - else if (_target.Master != null) //武器已经被其他角色拾起! - { - //再去寻找其他武器 - SetTargetWeapon(Master.FindTargetWeapon()); - - if (_target == null) //也没有其他可用的武器了 - { - ChangeState(GetNextState()); - } - } - else - { - //检测目标没有超出跟随视野距离 - _isInTailAfterRange = Master.IsInTailAfterViewRange(playerPos); - if (_isInTailAfterRange) - { - _tailAfterTimer = 0; - } - else - { - _tailAfterTimer += delta; - } - - //向武器移动 - if (!Master.NavigationAgent2D.IsNavigationFinished()) - { - //计算移动 - var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); - Master.AnimatedSprite.Play(AnimatorNames.Run); - Master.BasisVelocity = - (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * - Master.MoveSpeed; - } - else - { - Master.BasisVelocity = Vector2.Zero; - } - } - } - - private AiStateEnum GetNextState() - { - return _tailAfterTimer > 10 ? AiStateEnum.AiNormal : AiStateEnum.AiTailAfter; - } - - private void SetTargetWeapon(Weapon weapon) - { - _target = weapon; - //设置目标点 - if (_target != null) - { - Master.NavigationAgent2D.TargetPosition = _target.GlobalPosition; - } - } - - public override void DebugDraw() - { - if (_target != null) - { - Master.DrawLine(Vector2.Zero, Master.ToLocal(_target.GlobalPosition), Colors.Purple); - - if (_tailAfterTimer <= 0) - { - Master.DrawLine(Vector2.Zero, Master.ToLocal(Player.Current.GetCenterPosition()), Colors.Orange); - } - else if (_tailAfterTimer <= 10) - { - Master.DrawLine(Vector2.Zero, Master.ToLocal(Player.Current.GetCenterPosition()), Colors.Blue); - } - - } - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiFollowUpState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiFollowUpState.cs deleted file mode 100644 index ab05469..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiFollowUpState.cs +++ /dev/null @@ -1,126 +0,0 @@ - -using Godot; - -/// <summary> -/// 目标在视野内, 跟进目标, 如果距离在子弹有效射程内, 则开火 -/// </summary> -public class AiFollowUpState : StateBase<Enemy, AiStateEnum> -{ - - /// <summary> - /// 目标是否在视野内 - /// </summary> - public bool IsInView; - - //导航目标点刷新计时器 - private float _navigationUpdateTimer = 0; - private float _navigationInterval = 0.3f; - - public AiFollowUpState() : base(AiStateEnum.AiFollowUp) - { - } - - public override void Enter(AiStateEnum prev, params object[] args) - { - _navigationUpdateTimer = 0; - IsInView = true; - } - - public override void Process(float delta) - { - //先检查弹药是否打光 - if (Master.IsAllWeaponTotalAmmoEmpty()) - { - //再寻找是否有可用的武器 - var targetWeapon = Master.FindTargetWeapon(); - if (targetWeapon != null) - { - ChangeState(AiStateEnum.AiFindAmmo, targetWeapon); - return; - } - else - { - //切换到随机移动状态 - ChangeState(AiStateEnum.AiSurround); - } - } - - var playerPos = Player.Current.GetCenterPosition(); - - //更新玩家位置 - if (_navigationUpdateTimer <= 0) - { - //每隔一段时间秒更改目标位置 - _navigationUpdateTimer = _navigationInterval; - Master.NavigationAgent2D.TargetPosition = playerPos; - } - else - { - _navigationUpdateTimer -= delta; - } - - var masterPosition = Master.GlobalPosition; - - //是否在攻击范围内 - var inAttackRange = false; - - var weapon = Master.Holster.ActiveWeapon; - if (weapon != null) - { - inAttackRange = masterPosition.DistanceSquaredTo(playerPos) <= Mathf.Pow(Master.GetWeaponRange(0.7f), 2); - } - - //枪口指向玩家 - Master.LookTargetPosition(playerPos); - - if (!Master.NavigationAgent2D.IsNavigationFinished()) - { - //计算移动 - var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); - Master.AnimatedSprite.Play(AnimatorNames.Run); - Master.BasisVelocity = (nextPos - masterPosition - Master.NavigationPoint.Position).Normalized() * - Master.MoveSpeed; - } - else - { - Master.BasisVelocity = Vector2.Zero; - } - - //检测玩家是否在视野内 - if (Master.IsInTailAfterViewRange(playerPos)) - { - IsInView = !Master.TestViewRayCast(playerPos); - //关闭射线检测 - Master.TestViewRayCastOver(); - } - else - { - IsInView = false; - } - - if (IsInView) - { - if (inAttackRange) //在攻击范围内 - { - //发起攻击 - Master.EnemyAttack(delta); - - //距离够近, 可以切换到环绕模式 - if (Master.GlobalPosition.DistanceSquaredTo(playerPos) <= Mathf.Pow(weapon.Attribute.BulletMinDistance, 2) * 0.7f) - { - ChangeState(AiStateEnum.AiSurround); - } - } - } - else - { - ChangeState(AiStateEnum.AiTailAfter); - } - } - - public override void DebugDraw() - { - var playerPos = Player.Current.GetCenterPosition(); - Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Red); - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiLeaveForState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiLeaveForState.cs deleted file mode 100644 index 8185484..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiLeaveForState.cs +++ /dev/null @@ -1,101 +0,0 @@ - -using Godot; - -/// <summary> -/// 收到其他敌人通知, 前往发现目标的位置 -/// </summary> -public class AiLeaveForState : StateBase<Enemy, AiStateEnum> -{ - //导航目标点刷新计时器 - private float _navigationUpdateTimer = 0; - private float _navigationInterval = 0.3f; - - public AiLeaveForState() : base(AiStateEnum.AiLeaveFor) - { - } - - public override void Enter(AiStateEnum prev, params object[] args) - { - if (Master.World.Enemy_IsFindTarget) - { - Master.NavigationAgent2D.TargetPosition = Master.World.Enemy_FindTargetPosition; - } - else - { - ChangeState(prev); - return; - } - - //先检查弹药是否打光 - if (Master.IsAllWeaponTotalAmmoEmpty()) - { - //再寻找是否有可用的武器 - var targetWeapon = Master.FindTargetWeapon(); - if (targetWeapon != null) - { - ChangeState(AiStateEnum.AiFindAmmo, targetWeapon); - } - } - } - - public override void Process(float delta) - { - //这个状态下不会有攻击事件, 所以没必要每一帧检查是否弹药耗尽 - - //更新玩家位置 - if (_navigationUpdateTimer <= 0) - { - //每隔一段时间秒更改目标位置 - _navigationUpdateTimer = _navigationInterval; - Master.NavigationAgent2D.TargetPosition = Master.World.Enemy_FindTargetPosition; - } - else - { - _navigationUpdateTimer -= delta; - } - - if (!Master.NavigationAgent2D.IsNavigationFinished()) - { - //计算移动 - var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); - Master.LookTargetPosition(Master.World.Enemy_FindTargetPosition); - Master.AnimatedSprite.Play(AnimatorNames.Run); - Master.BasisVelocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * - Master.MoveSpeed; - } - else - { - Master.BasisVelocity = Vector2.Zero; - } - - var playerPos = Player.Current.GetCenterPosition(); - //检测玩家是否在视野内, 如果在, 则切换到 AiTargetInView 状态 - if (Master.IsInTailAfterViewRange(playerPos)) - { - if (!Master.TestViewRayCast(playerPos)) //看到玩家 - { - //关闭射线检测 - Master.TestViewRayCastOver(); - //切换成发现目标状态 - ChangeState(AiStateEnum.AiFollowUp); - return; - } - else - { - //关闭射线检测 - Master.TestViewRayCastOver(); - } - } - - //移动到目标掉了, 还没发现目标 - if (Master.NavigationAgent2D.IsNavigationFinished()) - { - ChangeState(AiStateEnum.AiNormal); - } - } - - public override void DebugDraw() - { - Master.DrawLine(Vector2.Zero, Master.ToLocal(Master.NavigationAgent2D.TargetPosition), Colors.Yellow); - } -} diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs deleted file mode 100644 index 299abf0..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs +++ /dev/null @@ -1,181 +0,0 @@ - -using Godot; - -/// <summary> -/// AI 正常状态 -/// </summary> -public class AiNormalState : StateBase<Enemy, AiStateEnum> -{ - //是否发现玩家 - private bool _isFindPlayer; - - //下一个运动的角度 - private Vector2 _nextPos; - - //是否移动结束 - private bool _isMoveOver; - - //上一次移动是否撞墙 - private bool _againstWall; - - //撞墙法线角度 - private float _againstWallNormalAngle; - - //移动停顿计时器 - private float _pauseTimer; - private bool _moveFlag; - - //上一帧位置 - private Vector2 _prevPos; - //卡在一个位置的时间 - private float _lockTimer; - - public AiNormalState() : base(AiStateEnum.AiNormal) - { - } - - public override void Enter(AiStateEnum prev, params object[] args) - { - _isFindPlayer = false; - _isMoveOver = true; - _againstWall = false; - _againstWallNormalAngle = 0; - _pauseTimer = 0; - _moveFlag = false; - } - - public override void Process(float delta) - { - //其他敌人发现玩家 - if (Master.CanChangeLeaveFor()) - { - ChangeState(AiStateEnum.AiLeaveFor); - return; - } - - if (_isFindPlayer) //已经找到玩家了 - { - //现临时处理, 直接切换状态 - ChangeState(AiStateEnum.AiTailAfter); - } - else //没有找到玩家 - { - //检测玩家 - var player = Player.Current; - //玩家中心点坐标 - var playerPos = player.GetCenterPosition(); - - if (Master.IsInViewRange(playerPos) && !Master.TestViewRayCast(playerPos)) //发现玩家 - { - //发现玩家 - _isFindPlayer = true; - } - else if (_pauseTimer >= 0) - { - Master.AnimatedSprite.Play(AnimatorNames.Idle); - _pauseTimer -= delta; - } - else if (_isMoveOver) //没发现玩家, 且已经移动完成 - { - RunOver(); - _isMoveOver = false; - } - else //移动中 - { - if (_lockTimer >= 1) //卡在一个点超过一秒 - { - RunOver(); - _isMoveOver = false; - _lockTimer = 0; - } - else if (Master.NavigationAgent2D.IsNavigationFinished()) //到达终点 - { - _pauseTimer = Utils.RandomRangeFloat(0.3f, 2f); - _isMoveOver = true; - _moveFlag = false; - Master.BasisVelocity = Vector2.Zero; - } - else if (!_moveFlag) - { - _moveFlag = true; - var pos = Master.GlobalPosition; - //计算移动 - var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); - Master.AnimatedSprite.Play(AnimatorNames.Run); - Master.BasisVelocity = (nextPos - pos - Master.NavigationPoint.Position).Normalized() * - Master.MoveSpeed; - _prevPos = pos; - } - else - { - var pos = Master.GlobalPosition; - var lastSlideCollision = Master.GetLastSlideCollision(); - if (lastSlideCollision != null && lastSlideCollision.GetCollider() is Role) //碰到其他角色 - { - _pauseTimer = Utils.RandomRangeFloat(0.1f, 0.5f); - _isMoveOver = true; - _moveFlag = false; - Master.BasisVelocity = Vector2.Zero; - } - else - { - //计算移动 - var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); - Master.AnimatedSprite.Play(AnimatorNames.Run); - Master.BasisVelocity = (nextPos - pos - Master.NavigationPoint.Position).Normalized() * - Master.MoveSpeed; - } - - if (_prevPos.DistanceSquaredTo(pos) <= 0.01f) - { - _lockTimer += delta; - } - else - { - _prevPos = pos; - } - } - } - - //关闭射线检测 - Master.TestViewRayCastOver(); - } - } - - //移动结束 - private void RunOver() - { - float angle; - if (_againstWall) - { - angle = Utils.RandomRangeFloat(_againstWallNormalAngle - Mathf.Pi * 0.5f, - _againstWallNormalAngle + Mathf.Pi * 0.5f); - } - else - { - angle = Utils.RandomRangeFloat(0, Mathf.Pi * 2f); - } - - var len = Utils.RandomRangeInt(30, 200); - _nextPos = new Vector2(len, 0).Rotated(angle) + Master.GlobalPosition; - //获取射线碰到的坐标 - if (Master.TestViewRayCast(_nextPos)) //碰到墙壁 - { - _nextPos = Master.ViewRay.GetCollisionPoint(); - _againstWall = true; - _againstWallNormalAngle = Master.ViewRay.GetCollisionNormal().Angle(); - } - else - { - _againstWall = false; - } - - Master.NavigationAgent2D.TargetPosition = _nextPos; - Master.LookTargetPosition(_nextPos); - } - - public override void DebugDraw() - { - Master.DrawLine(new Vector2(0, -8), Master.ToLocal(_nextPos), Colors.Green); - } -} diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiProbeState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiProbeState.cs deleted file mode 100644 index 1015095..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiProbeState.cs +++ /dev/null @@ -1,20 +0,0 @@ - -/// <summary> -/// Ai 不确定玩家位置 -/// </summary> -public class AiProbeState : StateBase<Enemy, AiStateEnum> -{ - public AiProbeState() : base(AiStateEnum.AiProbe) - { - } - - public override void Process(float delta) - { - //其他敌人发现玩家 - if (Master.CanChangeLeaveFor()) - { - ChangeState(AiStateEnum.AiLeaveFor); - return; - } - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiSurroundState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiSurroundState.cs deleted file mode 100644 index ebfce46..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiSurroundState.cs +++ /dev/null @@ -1,176 +0,0 @@ - -using Godot; - -/// <summary> -/// 距离目标足够近, 在目标附近随机移动, 并开火 -/// </summary> -public class AiSurroundState : StateBase<Enemy, AiStateEnum> -{ - /// <summary> - /// 目标是否在视野内 - /// </summary> - public bool IsInView = true; - - //是否移动结束 - private bool _isMoveOver; - - //移动停顿计时器 - private float _pauseTimer; - private bool _moveFlag; - - //下一个移动点 - private Vector2 _nextPosition; - - //上一帧位置 - private Vector2 _prevPos; - //卡在一个位置的时间 - private float _lockTimer; - - public AiSurroundState() : base(AiStateEnum.AiSurround) - { - } - - public override void Enter(AiStateEnum prev, params object[] args) - { - IsInView = true; - _isMoveOver = true; - _pauseTimer = 0; - _moveFlag = false; - } - - public override void Process(float delta) - { - //先检查弹药是否打光 - if (Master.IsAllWeaponTotalAmmoEmpty()) - { - //再寻找是否有可用的武器 - var targetWeapon = Master.FindTargetWeapon(); - if (targetWeapon != null) - { - ChangeState(AiStateEnum.AiFindAmmo, targetWeapon); - return; - } - } - - var playerPos = Player.Current.GetCenterPosition(); - var weapon = Master.Holster.ActiveWeapon; - - //枪口指向玩家 - Master.LookTargetPosition(playerPos); - - //检测玩家是否在视野内 - if (Master.IsInTailAfterViewRange(playerPos)) - { - IsInView = !Master.TestViewRayCast(playerPos); - //关闭射线检测 - Master.TestViewRayCastOver(); - } - else - { - IsInView = false; - } - - if (IsInView) - { - if (_pauseTimer >= 0) - { - Master.AnimatedSprite.Play(AnimatorNames.Idle); - _pauseTimer -= delta; - } - else if (_isMoveOver) //移动已经完成 - { - RunOver(playerPos); - _isMoveOver = false; - } - else - { - if (_lockTimer >= 1) //卡在一个点超过一秒 - { - RunOver(playerPos); - _isMoveOver = false; - _lockTimer = 0; - } - else if (Master.NavigationAgent2D.IsNavigationFinished()) //到达终点 - { - _pauseTimer = Utils.RandomRangeFloat(0f, 0.5f); - _isMoveOver = true; - _moveFlag = false; - Master.BasisVelocity = Vector2.Zero; - } - else if (!_moveFlag) - { - _moveFlag = true; - //计算移动 - var pos = Master.GlobalPosition; - var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); - Master.AnimatedSprite.Play(AnimatorNames.Run); - Master.BasisVelocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * - Master.MoveSpeed; - } - else - { - var pos = Master.GlobalPosition; - var lastSlideCollision = Master.GetLastSlideCollision(); - if (lastSlideCollision != null && lastSlideCollision.GetCollider() is Role) //碰到其他角色 - { - _pauseTimer = Utils.RandomRangeFloat(0f, 0.3f); - _isMoveOver = true; - _moveFlag = false; - Master.BasisVelocity = Vector2.Zero; - } - else - { - //计算移动 - var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); - Master.AnimatedSprite.Play(AnimatorNames.Run); - Master.BasisVelocity = (nextPos - pos - Master.NavigationPoint.Position).Normalized() * - Master.MoveSpeed; - } - - if (_prevPos.DistanceSquaredTo(pos) <= 0.01f) - { - _lockTimer += delta; - } - else - { - _prevPos = pos; - } - } - - if (weapon != null) - { - var position = Master.GlobalPosition; - if (position.DistanceSquaredTo(playerPos) > Mathf.Pow(Master.GetWeaponRange(0.7f), 2)) //玩家离开正常射击范围 - { - ChangeState(AiStateEnum.AiFollowUp); - } - else - { - //发起攻击 - Master.EnemyAttack(delta); - } - } - } - } - else //目标离开视野 - { - ChangeState(AiStateEnum.AiTailAfter); - } - } - - private void RunOver(Vector2 targetPos) - { - var weapon = Master.Holster.ActiveWeapon; - var distance = (int)(weapon == null ? 150 : (weapon.Attribute.BulletMinDistance * 0.7f)); - _nextPosition = new Vector2( - targetPos.X + Utils.RandomRangeInt(-distance, distance), - targetPos.Y + Utils.RandomRangeInt(-distance, distance) - ); - Master.NavigationAgent2D.TargetPosition = _nextPosition; - } - - public override void DebugDraw() - { - Master.DrawLine(new Vector2(0, -8), Master.ToLocal(_nextPosition), Colors.White); - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs deleted file mode 100644 index 87d018e..0000000 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs +++ /dev/null @@ -1,125 +0,0 @@ - -using Godot; - -/// <summary> -/// AI 发现玩家, 跟随玩家 -/// </summary> -public class AiTailAfterState : StateBase<Enemy, AiStateEnum> -{ - /// <summary> - /// 目标是否在视野半径内 - /// </summary> - private bool _isInViewRange; - - //导航目标点刷新计时器 - private float _navigationUpdateTimer = 0; - private float _navigationInterval = 0.3f; - - //目标从视野消失时已经过去的时间 - private float _viewTimer; - - public AiTailAfterState() : base(AiStateEnum.AiTailAfter) - { - } - - public override void Enter(AiStateEnum prev, params object[] args) - { - _isInViewRange = true; - _navigationUpdateTimer = 0; - _viewTimer = 0; - - //先检查弹药是否打光 - if (Master.IsAllWeaponTotalAmmoEmpty()) - { - //再寻找是否有可用的武器 - var targetWeapon = Master.FindTargetWeapon(); - if (targetWeapon != null) - { - ChangeState(AiStateEnum.AiFindAmmo, targetWeapon); - } - } - } - - public override void Process(float delta) - { - //这个状态下不会有攻击事件, 所以没必要每一帧检查是否弹药耗尽 - - var playerPos = Player.Current.GetCenterPosition(); - - //更新玩家位置 - if (_navigationUpdateTimer <= 0) - { - //每隔一段时间秒更改目标位置 - _navigationUpdateTimer = _navigationInterval; - Master.NavigationAgent2D.TargetPosition = playerPos; - } - else - { - _navigationUpdateTimer -= delta; - } - - //枪口指向玩家 - Master.LookTargetPosition(playerPos); - - if (!Master.NavigationAgent2D.IsNavigationFinished()) - { - //计算移动 - var nextPos = Master.NavigationAgent2D.GetNextPathPosition(); - Master.AnimatedSprite.Play(AnimatorNames.Run); - Master.BasisVelocity = (nextPos - Master.GlobalPosition - Master.NavigationPoint.Position).Normalized() * - Master.MoveSpeed; - } - else - { - Master.BasisVelocity = Vector2.Zero; - } - //检测玩家是否在视野内, 如果在, 则切换到 AiTargetInView 状态 - if (Master.IsInTailAfterViewRange(playerPos)) - { - if (!Master.TestViewRayCast(playerPos)) //看到玩家 - { - //关闭射线检测 - Master.TestViewRayCastOver(); - //切换成发现目标状态 - ChangeState(AiStateEnum.AiFollowUp); - return; - } - else - { - //关闭射线检测 - Master.TestViewRayCastOver(); - } - } - - //检测玩家是否在穿墙视野范围内, 直接检测距离即可 - _isInViewRange = Master.IsInViewRange(playerPos); - if (_isInViewRange) - { - _viewTimer = 0; - } - else //超出视野 - { - if (_viewTimer > 10) //10秒 - { - ChangeState(AiStateEnum.AiNormal); - } - else - { - _viewTimer += delta; - } - } - } - - public override void DebugDraw() - { - var playerPos = Player.Current.GetCenterPosition(); - if (_isInViewRange) - { - Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Orange); - } - else - { - Master.DrawLine(new Vector2(0, -8), Master.ToLocal(playerPos), Colors.Blue); - } - } -} \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/ui/roomUI/InteractiveTipBar.cs b/DungeonShooting_Godot/src/game/ui/roomUI/InteractiveTipBar.cs index b702af5..ff928a2 100644 --- a/DungeonShooting_Godot/src/game/ui/roomUI/InteractiveTipBar.cs +++ b/DungeonShooting_Godot/src/game/ui/roomUI/InteractiveTipBar.cs @@ -67,7 +67,8 @@ { var result = (CheckInteractiveResult)o; var interactiveItem = Player.Current.InteractiveItem; - if (interactiveItem is Weapon) + //if (interactiveItem is Weapon) + if (!string.IsNullOrEmpty(result.ShowIcon)) { _interactiveTarget = interactiveItem; //显示互动提示