diff --git a/DungeonShooting_Godot/prefab/role/Enemy.tscn b/DungeonShooting_Godot/prefab/role/Enemy.tscn
index 98ed763..c4fd32f 100644
--- a/DungeonShooting_Godot/prefab/role/Enemy.tscn
+++ b/DungeonShooting_Godot/prefab/role/Enemy.tscn
@@ -23,4 +23,7 @@
[node name="AnimatedSprite" parent="." index="2"]
material = SubResource( 2 )
-frame = 2
+frame = 0
+
+[node name="ViewRay" type="RayCast2D" parent="." index="7"]
+position = Vector2( 0, -8 )
diff --git a/DungeonShooting_Godot/src/framework/ActivityObject.cs b/DungeonShooting_Godot/src/framework/ActivityObject.cs
index 4de5ab2..71984fa 100644
--- a/DungeonShooting_Godot/src/framework/ActivityObject.cs
+++ b/DungeonShooting_Godot/src/framework/ActivityObject.cs
@@ -327,6 +327,10 @@
RestoreCollision();
}
+ ///
+ /// 往当前物体上挂载一个组件
+ ///
+ /// 组件对象
public void AddComponent(Component component)
{
if (!ContainsComponent(component))
@@ -337,6 +341,10 @@
}
}
+ ///
+ /// 移除一个组件
+ ///
+ /// 组件对象
public void RemoveComponent(Component component)
{
for (int i = 0; i < _components.Count; i++)
@@ -351,6 +359,9 @@
}
}
+ ///
+ /// 根据类型获取一个组件
+ ///
public Component GetComponent(Type type)
{
for (int i = 0; i < _components.Count; i++)
@@ -365,6 +376,9 @@
return null;
}
+ ///
+ /// 根据类型获取一个组件
+ ///
public TC GetComponent() where TC : Component
{
var component = GetComponent(typeof(TC));
diff --git a/DungeonShooting_Godot/src/framework/Component.cs b/DungeonShooting_Godot/src/framework/Component.cs
index 16c81e0..f81a600 100644
--- a/DungeonShooting_Godot/src/framework/Component.cs
+++ b/DungeonShooting_Godot/src/framework/Component.cs
@@ -2,7 +2,7 @@
using Godot;
///
-/// 组件基类, 用于挂载到游戏物体上, 相比于原生 Node 更加轻量化, 可以大量添加组件
+/// 组件基类, 用于挂载到游戏物体上, 相比于原生 Node 更加轻量化, 实例化 Component 不会创建额外的 Node, 可以大量添加组件
///
public abstract class Component : IProcess, IDestroy
{
diff --git a/DungeonShooting_Godot/src/game/GameApplication.cs b/DungeonShooting_Godot/src/game/GameApplication.cs
index 51aa4d2..6bb09c1 100644
--- a/DungeonShooting_Godot/src/game/GameApplication.cs
+++ b/DungeonShooting_Godot/src/game/GameApplication.cs
@@ -68,11 +68,17 @@
Ui.AddChild(Cursor);
}
+ ///
+ /// 将 viewport 以外的全局坐标 转换成 viewport 内的全局坐标
+ ///
public Vector2 GlobalToViewPosition(Vector2 globalPos)
{
return globalPos / GameConfig.WindowScale - (GameConfig.ViewportSize / 2) + GameCamera.Main.GlobalPosition;
}
+ ///
+ /// 将 viewport 以内的全局坐标 转换成 viewport 外的全局坐标
+ ///
public Vector2 ViewToGlobalPosition(Vector2 viewPos)
{
return (viewPos - GameCamera.Main.GlobalPosition + (GameConfig.ViewportSize / 2)) * GameConfig.WindowScale - GameCamera.Main.SubPixelPosition;
diff --git a/DungeonShooting_Godot/src/game/common/MathUtils.cs b/DungeonShooting_Godot/src/game/common/MathUtils.cs
deleted file mode 100644
index 9c6033a..0000000
--- a/DungeonShooting_Godot/src/game/common/MathUtils.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Godot;
-
-public static class MathUtils
-{
- ///
- /// 返回一个区间内的随机小数
- ///
- public static float RandRange(float min, float max)
- {
- if (min == max) return min;
- if (min > max)
- return GD.Randf() * (min - max) + max;
- return GD.Randf() * (max - min) + min;
- }
-
- ///
- /// 返回一个区间内的随机整数
- ///
- public static int RandRangeInt(int min, int max)
- {
- if (min == max) return min;
- if (min > max)
- return Mathf.FloorToInt(GD.Randf() * (min - max + 1) + max);
- return Mathf.FloorToInt(GD.Randf() * (max - min + 1) + min);
- }
-}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/common/NodeExtend.cs b/DungeonShooting_Godot/src/game/common/NodeExtend.cs
index bd54eee..f6db78c 100644
--- a/DungeonShooting_Godot/src/game/common/NodeExtend.cs
+++ b/DungeonShooting_Godot/src/game/common/NodeExtend.cs
@@ -1,5 +1,4 @@
using Godot;
-using System;
///
/// 该类为 node 节点通用扩展函数类
@@ -7,7 +6,7 @@
public static class NodeExtend
{
///
- /// 尝试将一个node2d节点转换成一个 ActivityObject 类
+ /// 尝试将一个 Node2d 节点转换成一个 ActivityObject 对象, 如果转换失败, 则返回 null
///
public static ActivityObject AsActivityObject(this Node2D node2d)
{
diff --git a/DungeonShooting_Godot/src/game/common/Utils.cs b/DungeonShooting_Godot/src/game/common/Utils.cs
new file mode 100644
index 0000000..153d6ca
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/common/Utils.cs
@@ -0,0 +1,29 @@
+using Godot;
+
+///
+/// 常用函数工具类
+///
+public static class Utils
+{
+ ///
+ /// 返回一个区间内的随机小数
+ ///
+ public static float RandRange(float min, float max)
+ {
+ if (min == max) return min;
+ if (min > max)
+ return GD.Randf() * (min - max) + max;
+ return GD.Randf() * (max - min) + min;
+ }
+
+ ///
+ /// 返回一个区间内的随机整数
+ ///
+ public static int RandRangeInt(int min, int max)
+ {
+ if (min == max) return min;
+ if (min > max)
+ return Mathf.FloorToInt(GD.Randf() * (min - max + 1) + max);
+ return Mathf.FloorToInt(GD.Randf() * (max - min + 1) + min);
+ }
+}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/item/weapon/Weapon.cs b/DungeonShooting_Godot/src/game/item/weapon/Weapon.cs
index 7680055..8f43ae9 100644
--- a/DungeonShooting_Godot/src/game/item/weapon/Weapon.cs
+++ b/DungeonShooting_Godot/src/game/item/weapon/Weapon.cs
@@ -418,7 +418,7 @@
if (!Attribute.ContinuousShoot)
{
continuousCount =
- MathUtils.RandRangeInt(Attribute.MinContinuousCount, Attribute.MaxContinuousCount);
+ Utils.RandRangeInt(Attribute.MinContinuousCount, Attribute.MaxContinuousCount);
}
}
@@ -475,7 +475,7 @@
OnFire();
//开火发射的子弹数量
- var bulletCount = MathUtils.RandRangeInt(Attribute.MaxFireBulletCount, Attribute.MinFireBulletCount);
+ var bulletCount = Utils.RandRangeInt(Attribute.MaxFireBulletCount, Attribute.MinFireBulletCount);
//武器口角度
var angle = new Vector2(GameConfig.ScatteringDistance, CurrScatteringRange).Angle();
@@ -502,7 +502,7 @@
//武器身位置
Position = new Vector2(
Mathf.Max(-Attribute.MaxBacklash,
- Position.x - MathUtils.RandRange(Attribute.MinBacklash, Attribute.MaxBacklash)), Position.y);
+ Position.x - Utils.RandRange(Attribute.MinBacklash, Attribute.MaxBacklash)), Position.y);
if (FireEvent != null)
{
@@ -711,8 +711,8 @@
if (flag)
{
Throw(new Vector2(30, 15), GlobalPosition, 0, 0,
- MathUtils.RandRangeInt(-20, 20), MathUtils.RandRangeInt(20, 50),
- MathUtils.RandRangeInt(-180, 180));
+ Utils.RandRangeInt(-20, 20), Utils.RandRangeInt(20, 50),
+ Utils.RandRangeInt(-180, 180));
}
}
else //没有武器
@@ -755,10 +755,10 @@
}
var startHeight = 6;
- var direction = master.GlobalRotationDegrees + MathUtils.RandRangeInt(-20, 20);
+ var direction = master.GlobalRotationDegrees + Utils.RandRangeInt(-20, 20);
var xf = 30;
- var yf = MathUtils.RandRangeInt(60, 120);
- var rotate = MathUtils.RandRangeInt(-180, 180);
+ var yf = Utils.RandRangeInt(60, 120);
+ var rotate = Utils.RandRangeInt(-180, 180);
Throw(new Vector2(30, 15), master.MountPoint.GlobalPosition, startHeight, direction, xf, yf, rotate, true);
}
diff --git a/DungeonShooting_Godot/src/game/item/weapon/bullet/Bullet.cs b/DungeonShooting_Godot/src/game/item/weapon/bullet/Bullet.cs
index 5e40c91..cfdffde 100644
--- a/DungeonShooting_Godot/src/game/item/weapon/bullet/Bullet.cs
+++ b/DungeonShooting_Godot/src/game/item/weapon/bullet/Bullet.cs
@@ -60,7 +60,7 @@
//播放受击动画
Node2D hit = ResourceManager.Load(ResourcePath.prefab_effect_Hit_tscn).Instance();
- hit.RotationDegrees = MathUtils.RandRangeInt(0, 360);
+ hit.RotationDegrees = Utils.RandRangeInt(0, 360);
hit.GlobalPosition = GlobalPosition;
GameApplication.Instance.Room.GetRoot(true).AddChild(hit);
diff --git a/DungeonShooting_Godot/src/game/item/weapon/gun/Gun.cs b/DungeonShooting_Godot/src/game/item/weapon/gun/Gun.cs
index c2d6da6..dd91ad0 100644
--- a/DungeonShooting_Godot/src/game/item/weapon/gun/Gun.cs
+++ b/DungeonShooting_Godot/src/game/item/weapon/gun/Gun.cs
@@ -93,10 +93,10 @@
//创建一个弹壳
var startPos = GlobalPosition + new Vector2(0, 5);
var startHeight = 6;
- var direction = GlobalRotationDegrees + MathUtils.RandRangeInt(-30, 30) + 180;
- var xf = MathUtils.RandRangeInt(20, 60);
- var yf = MathUtils.RandRangeInt(60, 120);
- var rotate = MathUtils.RandRangeInt(-720, 720);
+ var direction = GlobalRotationDegrees + Utils.RandRangeInt(-30, 30) + 180;
+ var xf = Utils.RandRangeInt(20, 60);
+ var yf = Utils.RandRangeInt(60, 120);
+ var rotate = Utils.RandRangeInt(-720, 720);
var shell = new ShellCase();
shell.Throw(new Vector2(10, 5), startPos, startHeight, direction, xf, yf, rotate, true);
@@ -115,7 +115,7 @@
//CreateBullet(BulletPack, FirePoint.GlobalPosition, fireRotation);
var bullet = new Bullet(
ResourcePath.prefab_weapon_bullet_Bullet_tscn,
- MathUtils.RandRange(Attribute.MinDistance, Attribute.MaxDistance),
+ Utils.RandRange(Attribute.MinDistance, Attribute.MaxDistance),
FirePoint.GlobalPosition,
fireRotation,
Master != null ? Master.AttackLayer : Role.DefaultAttackLayer
diff --git a/DungeonShooting_Godot/src/game/item/weapon/gun/Shotgun.cs b/DungeonShooting_Godot/src/game/item/weapon/gun/Shotgun.cs
index f1424b1..a8685d6 100644
--- a/DungeonShooting_Godot/src/game/item/weapon/gun/Shotgun.cs
+++ b/DungeonShooting_Godot/src/game/item/weapon/gun/Shotgun.cs
@@ -59,10 +59,10 @@
//创建一个弹壳
var startPos = GlobalPosition + new Vector2(0, 5);
var startHeight = 6;
- var direction = GlobalRotationDegrees + MathUtils.RandRangeInt(-30, 30) + 180;
- var xf = MathUtils.RandRangeInt(20, 60);
- var yf = MathUtils.RandRangeInt(60, 120);
- var rotate = MathUtils.RandRangeInt(-720, 720);
+ var direction = GlobalRotationDegrees + Utils.RandRangeInt(-30, 30) + 180;
+ var xf = Utils.RandRangeInt(20, 60);
+ var yf = Utils.RandRangeInt(60, 120);
+ var rotate = Utils.RandRangeInt(-720, 720);
var shell = new ShellCase();
shell.Throw(new Vector2(5, 10), startPos, startHeight, direction, xf, yf, rotate, true);
@@ -84,9 +84,9 @@
var bullet = new Bullet(
ResourcePath.prefab_weapon_bullet_Bullet_tscn,
- MathUtils.RandRange(Attribute.MinDistance, Attribute.MaxDistance),
+ Utils.RandRange(Attribute.MinDistance, Attribute.MaxDistance),
FirePoint.GlobalPosition,
- fireRotation + MathUtils.RandRange(-20 / 180f * Mathf.Pi, 20 / 180f * Mathf.Pi),
+ fireRotation + Utils.RandRange(-20 / 180f * Mathf.Pi, 20 / 180f * Mathf.Pi),
Master != null ? Master.AttackLayer : Role.DefaultAttackLayer
);
bullet.PutDown();
diff --git a/DungeonShooting_Godot/src/game/manager/InputManager.cs b/DungeonShooting_Godot/src/game/manager/InputManager.cs
index 9dc3121..359b9be 100644
--- a/DungeonShooting_Godot/src/game/manager/InputManager.cs
+++ b/DungeonShooting_Godot/src/game/manager/InputManager.cs
@@ -1,12 +1,15 @@
using Godot;
+///
+/// 输入管理器
+///
public static class InputManager
{
///
- /// 获取鼠标坐标
+ /// 获取鼠标在Viewport节点下的坐标
///
- public static Vector2 GetMousePosition()
+ public static Vector2 GetViewportMousePosition()
{
var application = GameApplication.Instance;
return application.GlobalToViewPosition(application.GetGlobalMousePosition());
diff --git a/DungeonShooting_Godot/src/game/role/Enemy.cs b/DungeonShooting_Godot/src/game/role/Enemy.cs
deleted file mode 100644
index 20b3a77..0000000
--- a/DungeonShooting_Godot/src/game/role/Enemy.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-
-public class Enemy : Role
-{
- public Enemy() : base(ResourcePath.prefab_role_Enemy_tscn)
- {
- AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Props | PhysicsLayer.Player;
- Camp = CampEnum.Camp2;
-
- MoveSpeed = 20;
- LookTarget = GameApplication.Instance.Room.Player;
- }
-
- public override void _Process(float delta)
- {
- base._Process(delta);
- Attack();
- }
-
- public override void _PhysicsProcess(float delta)
- {
- base._PhysicsProcess(delta);
-
- if (LookTarget != null)
- {
- AnimatedSprite.Animation = AnimatorNames.ReverseRun;
- Velocity = (LookTarget.GlobalPosition - GlobalPosition).Normalized() * MoveSpeed;
- CalcMove(delta);
- }
- }
-}
diff --git a/DungeonShooting_Godot/src/game/role/IState.cs b/DungeonShooting_Godot/src/game/role/IState.cs
new file mode 100644
index 0000000..14d609d
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/role/IState.cs
@@ -0,0 +1,44 @@
+///
+/// 状态接口
+///
+public interface IState where T : ActivityObject
+{
+ ///
+ /// 当前状态对象对应的状态枚举类型
+ ///
+ StateEnum StateType { get; }
+
+ ///
+ /// 当前状态对象挂载的角色对象
+ ///
+ T Master { get; set; }
+
+ ///
+ /// 当前状态对象所处的状态机对象
+ ///
+ StateController StateController { get; set; }
+
+ ///
+ /// 当从其他状态进入到当前状态时调用
+ ///
+ /// 上一个状态类型
+ /// 切换当前状态时附带的参数
+ void Enter(StateEnum prev, params object[] args);
+
+ ///
+ /// 物理帧每帧更新
+ ///
+ void PhysicsProcess(float delta);
+
+ ///
+ /// 是否允许切换至下一个状态
+ ///
+ /// 下一个状态类型
+ bool CanChangeState(StateEnum next);
+
+ ///
+ /// 从当前状态退出时调用
+ ///
+ /// 下一个状态类型
+ void Exit(StateEnum next);
+}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/role/Player.cs b/DungeonShooting_Godot/src/game/role/Player.cs
index 89b3681..8103e7f 100644
--- a/DungeonShooting_Godot/src/game/role/Player.cs
+++ b/DungeonShooting_Godot/src/game/role/Player.cs
@@ -48,7 +48,7 @@
var gPos = GlobalPosition;
if (LookTarget == null)
{
- Vector2 mousePos = InputManager.GetMousePosition();
+ Vector2 mousePos = InputManager.GetViewportMousePosition();
if (mousePos.x > gPos.x && Face == FaceDirection.Left)
{
Face = FaceDirection.Right;
diff --git a/DungeonShooting_Godot/src/game/role/Role.cs b/DungeonShooting_Godot/src/game/role/Role.cs
index 1387d2b..1cb7309 100644
--- a/DungeonShooting_Godot/src/game/role/Role.cs
+++ b/DungeonShooting_Godot/src/game/role/Role.cs
@@ -69,12 +69,12 @@
private FaceDirection _face;
///
- /// 是否启用角色移动
+ /// 是否启用角色移动, 如果禁用, 那么调用 CalcMove() 将不再有任何效果
///
public bool EnableMove { get; set; } = true;
///
- /// 移动速度
+ /// 移动速度, 通过调用 CalcMove() 函数来移动
///
public Vector2 Velocity { get; set; } = Vector2.Zero;
@@ -144,9 +144,9 @@
public ActivityObject LookTarget { get; set; }
///
- ///
+ /// 角色身上的状态机
///
- public StateCtr StateCtr { get; }
+ public StateController StateController { get; }
//初始缩放
private Vector2 _startScale;
@@ -218,6 +218,8 @@
public Role(string scenePath) : base(scenePath)
{
Holster = new Holster(this);
+ StateController = new StateController();
+ AddComponent(StateController);
}
public override void _Ready()
@@ -307,6 +309,16 @@
}
///
+ /// 判断指定坐标是否在角色视野方向
+ ///
+ public bool IsPositionInForward(Vector2 pos)
+ {
+ var gps = GlobalPosition;
+ return (Face == FaceDirection.Left && pos.x <= gps.x) ||
+ (Face == FaceDirection.Right && pos.x >= gps.x);
+ }
+
+ ///
/// 计算角色移动
///
public virtual void CalcMove(float delta)
diff --git a/DungeonShooting_Godot/src/game/role/StateController.cs b/DungeonShooting_Godot/src/game/role/StateController.cs
new file mode 100644
index 0000000..2ffc47a
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/role/StateController.cs
@@ -0,0 +1,109 @@
+using Godot;
+using System.Collections.Generic;
+
+///
+/// 对象状态机控制器
+///
+public class StateController : Component where T : ActivityObject
+{
+ ///
+ /// 当前活跃的状态
+ ///
+ public IState CurrState => _currState;
+ private IState _currState;
+
+ ///
+ /// 负责存放状态实例对象
+ ///
+ private readonly Dictionary> _states = new Dictionary>();
+
+ ///
+ /// 记录下当前帧是否有改变的状态
+ ///
+ private bool _isChangeState;
+
+ public override void PhysicsProcess(float delta)
+ {
+ _isChangeState = false;
+ if (CurrState != null)
+ {
+ CurrState.PhysicsProcess(delta);
+ //判断当前帧是否有改变的状态, 如果有, 则重新调用 PhysicsProcess() 方法
+ if (_isChangeState)
+ {
+ PhysicsProcess(delta);
+ }
+ }
+ }
+
+ ///
+ /// 往状态机力注册一个新的状态
+ ///
+ public void Register(IState state)
+ {
+ if (GetStateInstance(state.StateType) != null)
+ {
+ GD.PrintErr("当前状态已经在状态机中注册:", state);
+ return;
+ }
+ state.Master = ActivityObject as T;
+ state.StateController = this;
+ _states.Add(state.StateType, state);
+ }
+
+ ///
+ /// 立即切换到下一个指定状态, 并且这一帧会被调用 PhysicsProcess
+ ///
+ public void ChangeState(StateEnum next, params object[] arg)
+ {
+ _changeState(false, next, arg);
+ }
+
+ ///
+ /// 切换到下一个指定状态, 下一帧才会调用 PhysicsProcess
+ ///
+ public void ChangeStateLate(StateEnum next, params object[] arg)
+ {
+ _changeState(true, next, arg);
+ }
+
+ ///
+ /// 根据状态类型获取相应的状态对象
+ ///
+ private IState GetStateInstance(StateEnum stateType)
+ {
+ _states.TryGetValue(stateType, out var v);
+ return v;
+ }
+
+ ///
+ /// 切换状态
+ ///
+ private void _changeState(bool late, StateEnum next, params object[] arg)
+ {
+ if (_currState != null && _currState.StateType == next)
+ {
+ return;
+ }
+ var newState = GetStateInstance(next);
+ if (newState == null)
+ {
+ GD.PrintErr("当前状态机未找到相应状态:" + next);
+ return;
+ }
+ if (_currState == null)
+ {
+ _currState = newState;
+ newState.Enter(StateEnum.None, arg);
+ }
+ else if (_currState.CanChangeState(next))
+ {
+ _isChangeState = !late;
+ var prev = _currState.StateType;
+ _currState.Exit(next);
+ GD.Print("nextState => " + next);
+ _currState = newState;
+ _currState.Enter(prev, arg);
+ }
+ }
+}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/role/StateEnum.cs b/DungeonShooting_Godot/src/game/role/StateEnum.cs
new file mode 100644
index 0000000..3ba322d
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/role/StateEnum.cs
@@ -0,0 +1,20 @@
+
+public enum StateEnum
+{
+ ///
+ /// 无状态
+ ///
+ None = 0,
+ ///
+ /// 静止状态
+ ///
+ Idle = 1,
+ ///
+ /// 奔跑状态
+ ///
+ Run = 2,
+ ///
+ /// 行走状态
+ ///
+ Walk = 3,
+}
\ 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
new file mode 100644
index 0000000..060f075
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/role/enemy/Enemy.cs
@@ -0,0 +1,98 @@
+#region 基础敌人设计思路
+/*
+敌人有三种状态:
+状态1: 未发现玩家, 视野不可穿墙, 该状态下敌人移动比较规律, 移动速度较慢, 一旦玩家进入视野或者听到玩家枪声, 立刻切换至状态3, 该房间的敌人不能再回到状态1
+状态2: 发现有玩家, 但不知道在哪, 视野不可穿墙, 该情况下敌人移动速度明显加快, 移动不规律, 一旦玩家进入视野或者听到玩家枪声, 立刻切换至状态3
+状态3: 明确知道玩家的位置, 视野允许穿墙, 移动速度与状态2一致, 进入该状态时, 敌人之间会相互告知玩家所在位置, 并朝着玩家位置开火,
+ 如果有墙格挡, 则有一定概率继续开火, 一旦玩家立刻敌人视野超哥一段时间, 敌人自动切换为状态2
+
+敌人状态1只存在于少数房间内, 比如特殊房间, 大部分情况下敌人应该是状态2, 或者玩家进入房间时就被敌人发现
+*/
+#endregion
+
+
+using Godot;
+
+///
+/// 基础敌人
+///
+public class Enemy : Role
+{
+
+ ///
+ /// 视野半径, 单位像素
+ ///
+ public float ViewRange { get; set; } = 200;
+
+ ///
+ /// 背后的视野半径, 单位像素
+ ///
+ public float BackViewRange { get; set; } = 50;
+
+ ///
+ /// 视野检测射线, 朝玩家打射线, 检测是否碰到墙
+ ///
+ public RayCast2D ViewRay { get; }
+
+ public Enemy() : base(ResourcePath.prefab_role_Enemy_tscn)
+ {
+ AttackLayer = PhysicsLayer.Wall | PhysicsLayer.Props | PhysicsLayer.Player;
+ Camp = CampEnum.Camp2;
+
+ MoveSpeed = 20;
+
+ //视野射线
+ ViewRay = GetNode("ViewRay");
+
+ //注册Ai状态机
+ StateController.Register(new AIIdleState());
+ StateController.Register(new AIRunState());
+ //默认状态
+ StateController.ChangeState(StateEnum.Idle);
+ }
+
+ public override void _PhysicsProcess(float delta)
+ {
+ base._PhysicsProcess(delta);
+
+ var player = GameApplication.Instance.Room.Player;
+ //玩家中心点坐标
+ var playerPos = player.MountPoint.GlobalPosition;
+
+ //检测是否在视野内
+ var pos = GlobalPosition;
+
+ //玩家是否在前方
+ var isForward = IsPositionInForward(playerPos);
+
+ if (isForward) //脸朝向玩家
+ {
+ if (pos.DistanceSquaredTo(playerPos) <= ViewRange * ViewRange) //没有超出视野半径
+ {
+ //射线检测墙体
+ ViewRay.Enabled = true;
+ var localPos = ViewRay.ToLocal(playerPos);
+ ViewRay.CastTo = localPos;
+ ViewRay.ForceRaycastUpdate();
+
+ if (ViewRay.IsColliding())
+ {
+ LookTarget = null;
+ StateController.ChangeState(StateEnum.Idle);
+ }
+ else
+ {
+ LookTarget = player;
+ StateController.ChangeState(StateEnum.Run);
+ }
+
+ ViewRay.Enabled = false;
+ }
+ else
+ {
+ LookTarget = null;
+ StateController.ChangeState(StateEnum.Idle);
+ }
+ }
+ }
+}
diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AIIdleState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AIIdleState.cs
new file mode 100644
index 0000000..601d820
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/role/enemy/state/AIIdleState.cs
@@ -0,0 +1,29 @@
+
+///
+/// AI 静止行为
+///
+public class AIIdleState : IState
+{
+ public StateEnum StateType { get; } = StateEnum.Idle;
+ public Role Master { get; set; }
+ public StateController StateController { get; set; }
+ public void Enter(StateEnum prev, params object[] args)
+ {
+
+ }
+
+ public void PhysicsProcess(float delta)
+ {
+
+ }
+
+ public bool CanChangeState(StateEnum next)
+ {
+ return true;
+ }
+
+ public void Exit(StateEnum next)
+ {
+
+ }
+}
diff --git a/DungeonShooting_Godot/src/game/role/enemy/state/AIRunState.cs b/DungeonShooting_Godot/src/game/role/enemy/state/AIRunState.cs
new file mode 100644
index 0000000..2105c2f
--- /dev/null
+++ b/DungeonShooting_Godot/src/game/role/enemy/state/AIRunState.cs
@@ -0,0 +1,35 @@
+
+///
+/// AI 奔跑行为
+///
+public class AIRunState : IState
+{
+ public StateEnum StateType { get; } = StateEnum.Run;
+ public Role Master { get; set; }
+ public StateController StateController { get; set; }
+ public void Enter(StateEnum prev, params object[] args)
+ {
+
+ }
+
+ public void PhysicsProcess(float delta)
+ {
+ var master = Master;
+ if (master.LookTarget != null)
+ {
+ master.AnimatedSprite.Animation = AnimatorNames.ReverseRun;
+ master.Velocity = (master.LookTarget.GlobalPosition - master.GlobalPosition).Normalized() * master.MoveSpeed;
+ master.CalcMove(delta);
+ }
+ }
+
+ public bool CanChangeState(StateEnum next)
+ {
+ return true;
+ }
+
+ public void Exit(StateEnum next)
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/role/state/IState.cs b/DungeonShooting_Godot/src/game/role/state/IState.cs
deleted file mode 100644
index 8276dd0..0000000
--- a/DungeonShooting_Godot/src/game/role/state/IState.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-///
-/// 状态接口
-///
-public interface IState
-{
- ///
- /// 当前状态对象对应的状态枚举类型
- ///
- StateEnum StateType { get; }
-
- ///
- /// 当前状态对象挂载的角色对象
- ///
- Role Role { get; set; }
-
- ///
- /// 当前状态对象所处的状态机对象
- ///
- StateCtr StateController { get; set; }
-
- ///
- /// 当从其他状态进入到当前状态时调用
- ///
- /// 上一个状态类型
- /// 切换当前状态时附带的参数
- void Enter(StateEnum prev, params object[] args);
-
- ///
- /// 物理帧每帧更新
- ///
- void PhysicsProcess(float delta);
-
- ///
- /// 是否允许切换至下一个状态
- ///
- /// 下一个状态类型
- bool CanChangeState(StateEnum next);
-
- ///
- /// 从当前状态退出时调用
- ///
- /// 下一个状态类型
- void Exit(StateEnum next);
-}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/role/state/StateCtr.cs b/DungeonShooting_Godot/src/game/role/state/StateCtr.cs
deleted file mode 100644
index d0305ce..0000000
--- a/DungeonShooting_Godot/src/game/role/state/StateCtr.cs
+++ /dev/null
@@ -1,109 +0,0 @@
-using Godot;
-using System.Collections.Generic;
-
-///
-/// 角色状态机控制器
-///
-public class StateCtr : Component
-{
- ///
- /// 当前活跃的状态
- ///
- public IState CurrState => _currState;
- private IState _currState;
-
- ///
- /// 负责存放状态实例对象
- ///
- private readonly Dictionary _states = new Dictionary();
-
- ///
- /// 记录下当前帧是否有改变的状态
- ///
- private bool _isChangeState;
-
- public override void PhysicsProcess(float delta)
- {
- _isChangeState = false;
- if (CurrState != null)
- {
- CurrState.PhysicsProcess(delta);
- //判断当前帧是否有改变的状态, 如果有, 则重新调用 PhysicsProcess() 方法
- if (_isChangeState)
- {
- PhysicsProcess(delta);
- }
- }
- }
-
- ///
- /// 往状态机力注册一个新的状态
- ///
- public void Register(IState state)
- {
- if (GetStateInstance(state.StateType) != null)
- {
- GD.PrintErr("当前状态已经在状态机中注册:", state);
- return;
- }
- state.Role = ActivityObject as Role;
- state.StateController = this;
- _states.Add(state.StateType, state);
- }
-
- ///
- /// 立即切换到下一个指定状态, 并且这一帧会被调用 PhysicsProcess
- ///
- public void ChangeState(StateEnum next, params object[] arg)
- {
- _changeState(false, next, arg);
- }
-
- ///
- /// 切换到下一个指定状态, 下一帧才会调用 PhysicsProcess
- ///
- public void ChangeStateLate(StateEnum next, params object[] arg)
- {
- _changeState(true, next, arg);
- }
-
- ///
- /// 根据状态类型获取相应的状态对象
- ///
- private IState GetStateInstance(StateEnum stateType)
- {
- _states.TryGetValue(stateType, out var v);
- return v;
- }
-
- ///
- /// 切换状态
- ///
- private void _changeState(bool late, StateEnum next, params object[] arg)
- {
- if (_currState != null && _currState.StateType == next)
- {
- return;
- }
- var newState = GetStateInstance(next);
- if (newState == null)
- {
- GD.PrintErr("当前状态机未找到相应状态:" + next);
- return;
- }
- if (_currState == null)
- {
- _currState = newState;
- newState.Enter(StateEnum.None, arg);
- }
- else if (_currState.CanChangeState(next))
- {
- _isChangeState = !late;
- var prev = _currState.StateType;
- _currState.Exit(next);
- GD.Print("nextState => " + next);
- _currState = newState;
- _currState.Enter(prev, arg);
- }
- }
-}
\ No newline at end of file
diff --git a/DungeonShooting_Godot/src/game/role/state/StateEnum.cs b/DungeonShooting_Godot/src/game/role/state/StateEnum.cs
deleted file mode 100644
index 151b523..0000000
--- a/DungeonShooting_Godot/src/game/role/state/StateEnum.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-
-public enum StateEnum
-{
- None = 0,
- Idle = 1,
- Run = 2,
- Move = 3,
-}
\ No newline at end of file