目录
前言:
一、怪物需求定制
1、大致功能
2、具体设计
二、状态逻辑设计
1、有哪些状态
2、状态的转换
(1)追逐状态
(2)攻击状态
(3)懒散巡视状态
(4)返回状态
3、画个状态转换图
编辑
三、开始撸代码!
未完待续!
状态机是游戏开发中很常用的东西,GF中也有对应的模块FSM(有限状态机)。但是呢,前面我用到的玩家预制体里的控制脚本是一把梭的。。。而且代码是很久以前写的可读性很差。。。(就是说不想再重构了呗),然后在这个游戏里目前并没有AI怪物的需求。。。
所以为了强行练习使用GF的有限状态机模块,我准备写一个简单的怪物AI,使用FSM来完成。
怪物一开始在一个范围内来回巡视,当它发现玩家的时候,将会追赶和攻击玩家,但是当玩家移动到一定范围之外它就会放弃追逐。
如图所示,蓝色、红色范围是相对怪物的范围,绿色、紫色标识的范围是地图上的固定范围;一开始怪物在绿色范围内徘徊,当玩家进入它的蓝色范围的时候,怪物会追赶玩家;若玩家在追赶过程中进入了怪物的红色范围,则怪物进入攻击状态;若在追逐过程中怪物跑出了紫色范围,则怪物停止追逐并且返回绿色范围。
既然我们要用状态机实现,我们就得提前想好有哪些状态,状态转换的条件等。
由上面的具体设计不难写出一共有四个状态:
然后继续想想,在每个状态下遇到哪些条件会转换到哪些状态。。。(所以实际上程序员大多数时间是在撑着脑袋盯着屏幕发呆。。。)
在这个状态下,
如果检测到玩家进入了攻击范围,则进入攻击状态;
如果自己超过了追逐范围,则进入返回状态;
在这个状态下,
如果检测到玩家跑出了攻击范围,则进入追逐状态;
在这个状态下,
如果检测到玩家进入了发现范围,则进入追逐状态;
在这个状态下,
如果检测到玩家进入了发现范围,则进入追逐状态;
如果返回到了懒散巡视范围,则进入懒散巡视状态;
虽然说做到上面的分析就完全可以开始写代码了,但是这里为了更直观展示这个状态机,我画个简陋的图:
既然怪物的状态及其转换已经设计的差不多啦,那么下面就开始撸状态代码!
啊慢着慢着,好像怪物实体还没有写出来呢。。。。。。
所以先来写一下怪物实体!由于之前已经详细写过有关实体的内容,这里就很简略地飞快写一下!
啊怪物预制体就做好了,怪物是这样一个圆形片片绝对不是因为我懒不想画不想花时间太懒了之类的,而是因为游戏设定中怪物就是这......编不下去了,就是我懒。
然后就是写怪物实体数据类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace ShadowU
{
public class EnemyData : EntityData
{
///
/// Idle的圆心
///
public Vector3 Origin
{
get;
private set;
}
///
/// Idle的范围
///
public float IdleRange
{
get;
private set;
}
///
/// 追逐的范围
///
public float ChasingRange
{
get;
private set;
}
///
/// 发现玩家的范围
///
public float AlarmRange
{
get;
private set;
}
///
/// 攻击范围
///
public float AttackRange
{
get;
private set;
}
///
/// 移动速度
///
public float Speed
{
get;
private set;
}
public EnemyData(int entityId, int typeId) : base(entityId, typeId)
{
}
}
}
逻辑过会儿再写。
因为怪物也是实体,所以在实体配置表里面加一个,用于加载预制体:
然后EnemyData的属性也通过数据表进行配置:
写数据表行类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace ShadowU
{
public class DREnemy : DataRowBase
{
private int m_Id;
public override int Id
{
get { return m_Id; }
}
///
/// Idle的圆心
///
public Vector3 Origin
{
get;
private set;
}
///
/// Idle的范围
///
public float IdleRange
{
get;
private set;
}
///
/// 追逐的范围
///
public float ChasingRange
{
get;
private set;
}
///
/// 发现玩家的范围
///
public float AlarmRange
{
get;
private set;
}
///
/// 攻击范围
///
public float AttackRange
{
get;
private set;
}
///
/// 移动速度
///
public float Speed
{
get;
private set;
}
public override bool ParseDataRow(string dataRowString, object userData)
{
string[] columnStrings = dataRowString.Split(DataTableExtension.DataSplitSeparators);
for (int i = 0; i < columnStrings.Length; i++)
{
columnStrings[i] = columnStrings[i].Trim(DataTableExtension.DataTrimSeparators);
Log.Debug(columnStrings[i]);
Log.Debug(columnStrings[i].Length);
}
int index = 0;
index++;
m_Id = int.Parse(columnStrings[index++]);
index++;
string[] rowSpawnPosition = columnStrings[index++].Split('|');
Origin = new Vector3(float.Parse(rowSpawnPosition[0]), float.Parse(rowSpawnPosition[1]), float.Parse(rowSpawnPosition[2]));
IdleRange = float.Parse(columnStrings[index++]);
ChasingRange = float.Parse(columnStrings[index++]);
AlarmRange = float.Parse(columnStrings[index++]);
AttackRange = float.Parse(columnStrings[index++]);
Speed = float.Parse(columnStrings[index++]);
return true;
}
}
}
加载数据表:
把读取数据表配置信息赋值给实体数据的逻辑放在EnemyData的构造函数里:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityGameFramework.Runtime;
using GameFramework.DataTable;
namespace ShadowU
{
public class EnemyData : EntityData
{
///
/// Idle的圆心
///
public Vector3 Origin
{
get;
private set;
}
///
/// Idle的范围
///
public float IdleRange
{
get;
private set;
}
///
/// 追逐的范围
///
public float ChasingRange
{
get;
private set;
}
///
/// 发现玩家的范围
///
public float AlarmRange
{
get;
private set;
}
///
/// 攻击范围
///
public float AttackRange
{
get;
private set;
}
///
/// 移动速度
///
public float Speed
{
get;
private set;
}
public EnemyData(int entityId, int typeId) : base(entityId, typeId)
{
IDataTable dtEnemy = GameEntry.DataTable.GetDataTable();
DREnemy drEnemy = dtEnemy.GetDataRow(typeId);
Origin = drEnemy.Origin;
IdleRange = drEnemy.IdleRange;
ChasingRange = drEnemy.ChasingRange;
AlarmRange = drEnemy.AlarmRange;
AttackRange = drEnemy.AttackRange;
Speed = drEnemy.Speed;
Position = Origin;
}
}
}
然后写Enemy的实体逻辑类(状态机之后写所以这里只写了测试实体数据是否读取成功的逻辑):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace ShadowU
{
public class Enemy : EntityLogic
{
protected override void OnInit(object userData)
{
base.OnInit(userData);
CachedTransform.position = ((EnemyData)userData).Position;
}
protected override void OnShow(object userData)
{
base.OnShow(userData);
EnemyData enemyData = (EnemyData)userData;
Log.Debug("敌人加载成功");
Log.Info(enemyData.Origin);
Log.Info(enemyData.IdleRange);
Log.Info(enemyData.ChasingRange);
Log.Info(enemyData.AlarmRange);
Log.Info(enemyData.AttackRange);
Log.Info(enemyData.Speed);
}
protected override void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
base.OnUpdate(elapseSeconds, realElapseSeconds);
}
}
}
然后在实体扩展中扩展一个加载敌人的函数
然后在主流程调用测试一下:
运行!
成功!