diff --git a/DungeonShooting_Godot/prefab/role/Enemy.tscn b/DungeonShooting_Godot/prefab/role/Enemy.tscn index f4e9b41..ae7b5d5 100644 --- a/DungeonShooting_Godot/prefab/role/Enemy.tscn +++ b/DungeonShooting_Godot/prefab/role/Enemy.tscn @@ -24,6 +24,7 @@ [node name="AnimatedSprite" parent="." index="2"] material = SubResource( 2 ) +frame = 1 [node name="Collision" parent="." index="3"] position = Vector2( 0, -8 ) diff --git a/DungeonShooting_Godot/prefab/role/Player.tscn b/DungeonShooting_Godot/prefab/role/Player.tscn index 7bfb950..7154560 100644 --- a/DungeonShooting_Godot/prefab/role/Player.tscn +++ b/DungeonShooting_Godot/prefab/role/Player.tscn @@ -24,4 +24,4 @@ [node name="AnimatedSprite" parent="." index="2"] material = SubResource( 2 ) -frame = 3 +frame = 1 diff --git a/DungeonShooting_Godot/src/framework/ActivityObject.cs b/DungeonShooting_Godot/src/framework/ActivityObject.cs index 089ca37..effd461 100644 --- a/DungeonShooting_Godot/src/framework/ActivityObject.cs +++ b/DungeonShooting_Godot/src/framework/ActivityObject.cs @@ -10,6 +10,11 @@ public abstract class ActivityObject : KinematicBody2D { /// + /// 是否是调试模式 + /// + public static bool IsDebug { get; set; } + + /// /// 当前物体类型id, 用于区分是否是同一种物体, 如果不是通过 ActivityObject.Create() 函数创建出来的对象那么 ItemId 为 null /// public string ItemId { get; internal set; } @@ -231,6 +236,13 @@ } /// + /// 如果开启 debug, 则每帧调用该函数, 可用于绘制文字线段等 + /// + protected virtual void DebugDraw() + { + } + + /// /// 拾起一个 node 节点 /// public void Pickup() @@ -408,9 +420,10 @@ var temp = arr[i].Value; if (temp != null && temp.ActivityObject == this && temp.Enable) { - if (!temp.IsStart) + if (!temp.IsReady) { temp.Ready(); + temp.IsReady = true; } temp.Process(delta); @@ -500,7 +513,54 @@ //计算阴影 CalcShadow(); } + + //调试绘制 + if (IsDebug) + { + Update(); + } + } + + public override void _PhysicsProcess(float delta) + { + //更新组件 + if (_components.Count > 0) + { + var arr = _components.ToArray(); + for (int i = 0; i < arr.Length; i++) + { + if (IsDestroyed) return; + var temp = arr[i].Value; + if (temp != null && temp.ActivityObject == this && temp.Enable) + { + if (!temp.IsReady) + { + temp.Ready(); + temp.IsReady = true; + } + temp.PhysicsProcess(delta); + } + } + } + } + + public override void _Draw() + { + if (IsDebug) + { + DebugDraw(); + var arr = _components.ToArray(); + for (int i = 0; i < arr.Length; i++) + { + if (IsDestroyed) return; + var temp = arr[i].Value; + if (temp != null && temp.ActivityObject == this && temp.Enable) + { + temp.DebugDraw(); + } + } + } } /// @@ -524,29 +584,7 @@ } } - public override void _PhysicsProcess(float delta) - { - //更新组件 - if (_components.Count > 0) - { - var arr = _components.ToArray(); - for (int i = 0; i < arr.Length; i++) - { - if (IsDestroyed) return; - var temp = arr[i].Value; - if (temp != null && temp.ActivityObject == this && temp.Enable) - { - if (!temp.IsStart) - { - temp.Ready(); - } - - temp.PhysicsProcess(delta); - } - } - } - } - + /// /// 销毁物体 /// diff --git a/DungeonShooting_Godot/src/framework/Component.cs b/DungeonShooting_Godot/src/framework/Component.cs index f81a600..2f2264a 100644 --- a/DungeonShooting_Godot/src/framework/Component.cs +++ b/DungeonShooting_Godot/src/framework/Component.cs @@ -142,8 +142,10 @@ /// public bool IsDestroyed { get; private set; } - //是否调用过 start 函数 - internal bool IsStart = false; + /// + /// 是否调用过 Ready 函数 + /// + public bool IsReady { get; set; } /// /// 第一次调用 Process 或 PhysicsProcess 之前调用 @@ -202,6 +204,13 @@ public virtual void OnDisable() { } + + /// + /// 如果开启 debug, 则每帧调用该函数, 可用于绘制文字线段等 + /// + public virtual void DebugDraw() + { + } /// /// 当组件销毁 diff --git a/DungeonShooting_Godot/src/game/GameApplication.cs b/DungeonShooting_Godot/src/game/GameApplication.cs index f53ca90..587b2f2 100644 --- a/DungeonShooting_Godot/src/game/GameApplication.cs +++ b/DungeonShooting_Godot/src/game/GameApplication.cs @@ -61,6 +61,8 @@ public override void _EnterTree() { + ActivityObject.IsDebug = Debug; + GlobalNodeRoot = GetNode(GlobalNodeRootPath); // 初始化鼠标 Cursor = CursorPack.Instance(); diff --git a/DungeonShooting_Godot/src/game/role/StateBase.cs b/DungeonShooting_Godot/src/game/role/StateBase.cs index 531f049..d2a7586 100644 --- a/DungeonShooting_Godot/src/game/role/StateBase.cs +++ b/DungeonShooting_Godot/src/game/role/StateBase.cs @@ -6,9 +6,14 @@ public abstract class StateBase where T : ActivityObject where S : Enum { /// - /// 当前状态对象对应的状态枚举类型 + /// 当前活跃的状态对象实例 /// - public S StateType { get; } + public StateBase CurrStateBase => StateController.CurrStateBase; + + /// + /// 当前对象对应的状态枚举 + /// + public S State { get; } /// /// 当前状态对象挂载的角色对象 @@ -22,7 +27,7 @@ public StateBase(S state) { - StateType = state; + State = state; } /// @@ -36,7 +41,7 @@ } /// - /// 物理帧每帧更新 + /// 如果当前状态已被激活, 物理帧每帧更新 /// public virtual void PhysicsProcess(float delta) { @@ -60,4 +65,28 @@ { } + + /// + /// 当启用 debug 后调用该函数, 调试绘制, 需要调用 Master 身上的绘制函数 + /// + public virtual void DebugDraw() + { + + } + + /// + /// 立即切换到下一个指定状态, 并且这一帧会被调用 PhysicsProcess + /// + public void ChangeState(S next, params object[] args) + { + StateController.ChangeState(next, args); + } + + /// + /// 切换到下一个指定状态, 下一帧才会调用 PhysicsProcess + /// + public void ChangeStateLate(S next, params object[] args) + { + StateController.ChangeStateLate(next, args); + } } \ No newline at end of file diff --git a/DungeonShooting_Godot/src/game/role/StateController.cs b/DungeonShooting_Godot/src/game/role/StateController.cs index ad9557d..f12bca1 100644 --- a/DungeonShooting_Godot/src/game/role/StateController.cs +++ b/DungeonShooting_Godot/src/game/role/StateController.cs @@ -8,11 +8,10 @@ public class StateController : Component where T : ActivityObject where S : Enum { /// - /// 当前活跃的状态 + /// 当前活跃的状态对象实例 /// - public StateBase CurrStateBase => _currStateBase; - private StateBase _currStateBase; - + public StateBase CurrStateBase { get; private set; } + /// /// 负责存放状态实例对象 /// @@ -22,7 +21,7 @@ /// 记录下当前帧是否有改变的状态 /// private bool _isChangeState; - + public override void PhysicsProcess(float delta) { _isChangeState = false; @@ -37,19 +36,28 @@ } } + public override void DebugDraw() + { + if (CurrStateBase != null) + { + CurrStateBase.DebugDraw(); + } + } + /// - /// 往状态机力注册一个新的状态 + /// 往状态机里注册一个新的状态实例 /// public void Register(StateBase stateBase) { - if (GetStateInstance(stateBase.StateType) != null) + if (GetStateInstance(stateBase.State) != null) { GD.PrintErr("当前状态已经在状态机中注册:", stateBase); return; } + stateBase.Master = ActivityObject as T; stateBase.StateController = this; - _states.Add(stateBase.StateType, stateBase); + _states.Add(stateBase.State, stateBase); } /// @@ -82,29 +90,31 @@ /// private void _changeState(bool late, S next, params object[] arg) { - if (_currStateBase != null && _currStateBase.StateType.Equals(next)) + if (CurrStateBase != null && CurrStateBase.State.Equals(next)) { return; } + var newState = GetStateInstance(next); if (newState == null) { GD.PrintErr("当前状态机未找到相应状态:" + next); return; } - if (_currStateBase == null) + + if (CurrStateBase == null) { - _currStateBase = newState; + CurrStateBase = newState; newState.Enter(default, arg); } - else if (_currStateBase.CanChangeState(next)) + else if (CurrStateBase.CanChangeState(next)) { _isChangeState = !late; - var prev = _currStateBase.StateType; - _currStateBase.Exit(next); + var prev = CurrStateBase.State; + CurrStateBase.Exit(next); GD.Print("nextState => " + next); - _currStateBase = newState; - _currStateBase.Enter(prev, arg); + CurrStateBase = newState; + CurrStateBase.Enter(prev, arg); } } } \ 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 index 720a970..dc0bfb4 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs @@ -30,6 +30,11 @@ public float ViewRange { get; set; } = 200; /// + /// 发现玩家后的视野半径 + /// + public float TailAfterViewRange { get; set; } = 250; + + /// /// 背后的视野半径, 单位像素 /// public float BackViewRange { get; set; } = 50; diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs index 6939a45..8eb006b 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/state/AiNormalState.cs @@ -20,7 +20,7 @@ if (Master.IsInViewRange(playerPos) && Master.TestViewRayCast(playerPos) == null) { //发现玩家 - StateController.ChangeStateLate(AIStateEnum.AITailAfter); + ChangeStateLate(AIStateEnum.AITailAfter); } } } diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs index 5fe44e0..07396bc 100644 --- a/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs +++ b/DungeonShooting_Godot/src/game/role/enemy/state/AiTailAfterState.cs @@ -1,32 +1,47 @@  +using Godot; + /// /// AI 发现玩家 /// public class AiTailAfterState : StateBase { - + //导航目标点刷新计时器 private float _navigationUpdateTimer = 0; + private float _navigationInterval = 0.3f; + + private bool _isInView = true; + + private float _viewTimer = 0; public AiTailAfterState() : base(AIStateEnum.AITailAfter) { } + public override void Enter(AIStateEnum prev, params object[] args) + { + _isInView = true; + _navigationUpdateTimer = 0; + } + public override void PhysicsProcess(float delta) { - //跟随玩家 var master = Master; if (master.NavigationAgent2D.IsNavigationFinished()) { return; } - var playerGlobalPosition = GameApplication.Instance.Room.Player.GlobalPosition; - //临时处理, 让敌人跟随玩家 + var masterPos = master.GlobalPosition; + var playerPos = GameApplication.Instance.Room.Player.GlobalPosition; + + //更新玩家位置 if (_navigationUpdateTimer <= 0) { - _navigationUpdateTimer = 0.2f; - if (master.NavigationAgent2D.GetTargetLocation() != playerGlobalPosition) + //每隔一段时间秒更改目标位置 + _navigationUpdateTimer = _navigationInterval; + if (master.NavigationAgent2D.GetTargetLocation() != playerPos) { - master.NavigationAgent2D.SetTargetLocation(playerGlobalPosition); + master.NavigationAgent2D.SetTargetLocation(playerPos); } } else @@ -34,10 +49,42 @@ _navigationUpdateTimer -= delta; } + //计算移动 var nextPos = master.NavigationAgent2D.GetNextLocation(); - master.LookTargetPosition(playerGlobalPosition); + master.LookTargetPosition(playerPos); master.AnimatedSprite.Animation = AnimatorNames.Run; master.Velocity = (nextPos - master.GlobalPosition - master.NavigationPoint.Position).Normalized() * master.MoveSpeed; master.CalcMove(delta); + + //检测玩家是否在视野内, 此时视野可穿墙, 直接检测距离即可 + _isInView = masterPos.DistanceSquaredTo(playerPos) <= master.TailAfterViewRange * master.TailAfterViewRange; + if (_isInView) + { + _viewTimer = 0; + } + else //超出视野 + { + if (_viewTimer > 10) //10秒 + { + ChangeStateLate(AIStateEnum.AIProbe); + } + else + { + _viewTimer += delta; + } + } + } + + public override void DebugDraw() + { + var playerPos = GameApplication.Instance.Room.Player.GlobalPosition; + if (_isInView) + { + Master.DrawLine(Vector2.Zero, Master.ToLocal(playerPos), Colors.Red); + } + else + { + Master.DrawLine(Vector2.Zero, Master.ToLocal(playerPos), Colors.Blue); + } } } \ No newline at end of file