目录
前言
在上一作魔鬼与牧师MVC版本的基本思想上制作一般动作分离的魔鬼与牧师游戏,故游戏本身的分析与上一作一致,只有代码思路不一样。
具体游戏分析请看:
成果展示与获取
获取:https://gitee.com/Wesley-L/the-devil-and-the-pastor-action-separate.git
动作分离
在游戏设计中,动作分离是一种技术和方法,用于将游戏中的动作与角色或对象的状态分离开来。它可以帮助开发者更好地控制和管理游戏中的动作,提供更灵活和可扩展的游戏玩法。
动作分离的基本思想是将角色或对象的动作定义为独立的动作资源,而不是直接嵌入到角色或对象的状态中。这些动作资源可以是预先录制的动画片段,也可以是程序生成的动画。通过这种方式,开发者可以根据需要组合和控制这些动作资源,实现更丰富和多样化的游戏动作。
动作分离的好处包括:
-
灵活性:通过将动作与角色状态分离,开发者可以更加灵活地组合和切换不同的动作,从而实现更多样化的游戏玩法和交互方式。
-
可扩展性:动作分离使得添加新的动作变得更加容易,开发者可以简单地添加新的动作资源,而无需修改现有的游戏逻辑和代码。
-
可复用性:由于动作被独立定义和管理,它们可以在不同的角色或对象之间共享和重用,从而提高开发效率和资源利用率。
-
精细控制:动作分离使得开发者可以更精细地控制动作的细节,例如动画过渡、播放速度、循环方式等,从而实现更好的游戏体验。
在实际应用中,动作分离通常需要使用合适的工具和技术,例如动画编辑器、动作状态机等。开发者需要设计和定义角色的状态和转换条件,并将相应的动作资源与之关联起来。通过合理的状态管理和动作调度,可以实现流畅和逼真的游戏动作。
动作分离设计
设计思路
动作设计
ActionController
回调函数接口
这里采用接口来表示,每一个动作完成后,调用者会进行的操作,这一操作被放置在了回调函数里。
public enum SSActionEventType : int { Started, Competeted }
//接口
public interface ISSActionCallback {
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null);
}
动作基类
这里通过基类和虚函数来规定子类需要实现的操作,通过“可进行”和“可删除”两个变量来控制动作。
//动作基类
public class SSAction : ScriptableObject {
public bool enable = true; //是否可进行
public bool destroy = false; //是否已完成
public GameObject gameobject; //动作对象
public Transform transform; //动作对象的transform
public ISSActionCallback callback; //回调函数
/*防止用户自己new对象*/
protected SSAction() { }
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
单个动作
注意单个动作的以下两个操作:
//这里利用Unity自带的构造函数创建对象,并初始化
public static SSMoveToAction GetSSAction(Vector3 _target, float _speed) {
SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();
action.target = _target;
action.speed = _speed;
return action;
}
public override void Update() {
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
//动作完成,通知动作管理者或动作组合
if (this.transform.position == target) {
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
组合动作
组合动作包括:
public List<SSAction> sequence; //动作的列表
public int repeat = -1; //-1就是无限循环
public int start = 0; //当前做的动作的标号
注意以下几个函数:
//让序列中的每一个动作都执行
public override void Update() {
if (sequence.Count == 0) return;
if (start < sequence.Count) {
sequence[start].Update(); //执行之后,通过回调函数让start值递增
}
}
//回调函数
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null) {
source.destroy = false; //由于可能还会再次调用,因此先不删除
this.start++;
if (this.start >= sequence.Count) {
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0) {
this.destroy = true; //删除
this.callback.SSActionEvent(this); //通知管理者
}
}
}
动作管理器基类
动作管理器是一个门面,它可以自己实现对于动作的控制,并可以提供接口给外部函数调用。 尤其注意这里的等待添加和等待删除的队列,由于每次update的过程中不可能立刻对新增动作和已执行的动作进行处理,因此需要一个队列来进行缓冲。被删除的动作对象也由Unity的Destroy进行操作。 对于门面模式,这里提供了函数RunAction()
,外界仅需指定游戏对象、动作和回调处理器即可由这个类完成这些动作的实现。
public class SSActionManager : MonoBehaviour {
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //动作字典
private List<SSAction> waitingAdd = new List<SSAction>(); //等待执行的动作列表
private List<int> waitingDelete = new List<int>(); //等待删除的key列表
protected void Update() {
//将等待执行的动作加入字典并清空待执行列表
foreach (SSAction ac in waitingAdd) {
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();
//对于字典中每一个动作,看是执行还是删除
foreach (KeyValuePair<int, SSAction> kv in actions) {
SSAction ac = kv.Value;
if (ac.destroy) {
waitingDelete.Add(ac.GetInstanceID());
}
else if (ac.enable) {
ac.Update();//可能是组合动作的执行,也可能是单个动作的执行
}
}
//删除所有已完成的动作并清空待删除列表
foreach (int key in waitingDelete) {
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);//让Unity帮着删除
}
waitingDelete.Clear();
}
//外界只需要调用动作管理类的RunAction函数即可完成动作。
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) {
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
}
本次游戏动作管理器实例
注意,这里对移动船和移动对象又进行了封装,现在用户只需提供对象、终点和速度即可。
public void moveBoat(GameObject boat, Vector3 end, float speed) {
boatMove = SSMoveToAction.GetSSAction(end, speed);
this.RunAction(boat, boatMove, this);
}
public void moveRole(GameObject role, Vector3 middle, Vector3 end, float speed) {
//两段移动
SSAction action1 = SSMoveToAction.GetSSAction(middle, speed);
SSAction action2 = SSMoveToAction.GetSSAction(end, speed);
//两个动作,都只重复一次
roleMove = SequenceAction.GetSSAcition(1, 0, new List<SSAction> { action1, action2 });
this.RunAction(role, roleMove, this);
}
裁判类
只需将原有的判断函数移动到类中即可,作一些变量的修改:
public class GameCheck : MonoBehaviour {
public Controller sceneController;
protected void Start() {
sceneController = (Controller)SSDirector.GetInstance().CurrentScenceController;
}
public int GameJudge()
{
int src_priest = sceneController.src_land.GetTotal(0);
int src_ghost = sceneController.src_land.GetTotal(1);
int des_priest = sceneController.des_land.GetTotal(0);
int des_ghost = sceneController.des_land.GetTotal(1);
if (des_priest + des_ghost == 6)
{ //全部到终点,获胜
return 1;
}
if (sceneController.boat.GetBoatMark() == 1)//由于在这一边船还没开,因此只需检测另一边的数量即可。
{
if (des_priest < des_ghost && des_priest > 0)
{//失败
return -1;
}
}
else
{
if (src_priest < src_ghost && src_priest > 0)
{//失败
return -1;
}
}
return 0;//游戏继续进行
}
}
模型设计
代码中的主要类和接口包括:
-
ISceneController
接口:定义了一个加载场景资源的方法LoadResources()
。 -
IUserAction
接口:定义了用户操作相关的方法,包括移动船只(MoveBoat()
)、重新开始游戏(Restart()
)和移动角色(MoveRole(RoleModel role)
)。 -
SSDirector
类:导演类,用于管理当前场景控制器和实现单例模式。 -
RoleModel
类:角色模型类,表示游戏中的角色。每个角色都有一个角色标识(role_sign
),可以是牧师或魔鬼,以及一个表示是否在船上的状态(on_boat
)。角色可以在陆地和船只之间移动,具有移动方法(Move(Vector3 end)
)和上下船的方法(ToLand(LandModel land)
和ToBoat(BoatModel boat)
)。 -
LandModel
类:陆地模型类,表示游戏中的陆地。每个陆地对象有一个标识(land_mark
),用于区分起始陆地和目标陆地。陆地上可以放置角色,通过AddRole(RoleModel role)
方法添加角色,通过RemoveRole(string name)
方法移除角色。还提供了获取空位置和计算特定类型角色数量的方法。 -
BoatModel
类:船只模型类,表示游戏中的船只。船只有一个标识(boat_mark
),用于区分船只所在的陆地。船上可以放置两个角色,通过AddRole(RoleModel role)
方法添加角色,通过RemoveRole(string name)
方法移除角色。还提供了获取空位置和计算船上角色数量的方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace GhostBoatGame
{
//加载场景接口
public interface ISceneController
{
void LoadResources();
}
//用户操作接口
public interface IUserAction
{
void MoveBoat(); //移动船
void Restart(); //重新开始
void MoveRole(RoleModel role); //移动角色
}
//导演类
public class SSDirector : System.Object
{
private static SSDirector _instance;
public ISceneController CurrentScenceController { get; set; }
public static SSDirector GetInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
public class RoleModel
{
GameObject role;
int role_sign; //0为牧师,1为魔鬼
bool on_boat; //是否在船上
LandModel land = (SSDirector.GetInstance().CurrentScenceController as Controller).src_land;//所在的陆地
GhostBoatActionManager moveController;
Clickable click;//给model赋予属性,可点击、可移动,相当于添加了脚本。
public float speed = 15;
public RoleModel(int id, Vector3 pos)
{
if (id == 0)
{
role_sign = 0;
role = Object.Instantiate(Resources.Load("Prefabs/Priest", typeof(GameObject)), pos, Quaternion.identity) as GameObject;
}
else
{
role_sign = 1;
role = Object.Instantiate(Resources.Load("Prefabs/Ghost", typeof(GameObject)), pos, Quaternion.identity) as GameObject;
}
click = role.AddComponent(typeof(Clickable)) as Clickable;
click.SetRole(this);
moveController = (SSDirector.GetInstance().CurrentScenceController as Controller).action_manager;
}
public int GetSign()
{
return role_sign;
}
public string GetName()
{
return role.name;
}
public LandModel GetLandModel()
{
return land;
}
public bool IsOnBoat()
{
return on_boat;
}
public void SetName(string name)
{
role.name = name;
}
public void Move(Vector3 end)//移动操作的对外接口
{
Vector3 middle = new Vector3(role.transform.position.x, end.y, end.z);
moveController.moveRole(role, middle, end, speed);
}
public void ToLand(LandModel land)//上岸的移动
{
Vector3 pos = land.GetEmptyPosition();
Vector3 middle = new Vector3(role.transform.position.x, pos.y, pos.z);
moveController.moveRole(role, middle, pos, speed);
this.land = land;
on_boat = false;
}
public void ToBoat(BoatModel boat)//上船的移动
{
Vector3 pos = boat.GetEmptyPosition();
Vector3 middle = new Vector3(pos.x, role.transform.position.y, pos.z);
moveController.moveRole(role, middle, pos, speed);
this.land = null;
on_boat = true;
}
public void Reset()
{
LandModel land = (SSDirector.GetInstance().CurrentScenceController as Controller).src_land;
ToLand(land);
land.AddRole(this);
}
}
public class LandModel
{
GameObject land; //陆地对象
public int land_mark;//src为1,des为-1。
RoleModel[] roles = new RoleModel[6];//陆地上的角色对象
Vector3[] role_positions;//每个角色的位置
public LandModel(int sign)
{//根据对象标识初始化
land_mark = sign;
land = Object.Instantiate(Resources.Load("Prefabs/Land", typeof(GameObject)), new Vector3(10.5F * land_mark, 0.5F, 0), Quaternion.identity) as GameObject;
role_positions = new Vector3[] { new Vector3(6.5F * land_mark, 1.8F, 0), new Vector3(8.0F * land_mark, 1.8F, 0), new Vector3(9.5F * land_mark, 1.8F, 0), new Vector3(11.0F * land_mark, 1.8F, 0), new Vector3(12.5F * land_mark, 1.8F, 0), new Vector3(14.0F * land_mark, 1.8F, 0) };
}
public int GetLandMark()
{
return land_mark;
}
public Vector3 GetEmptyPosition()
{//找到当前空位置
int pos = -1;
for (int i = 0; i < 6; i++)
{
if (roles[i] == null)
{
pos = i;
break;
}
}
return role_positions[pos];
}
public void AddRole(RoleModel role)
{//添加role
for (int i = 0; i < 6; i++)
{
if (roles[i] == null)
{
roles[i] = role;
break;
}
}
}
public RoleModel RemoveRole(string name)
{//删除role
for (int i = 0; i < 6; i++)
{
if (roles[i] != null && roles[i].GetName() == name)
{
roles[i] = null;
return roles[i];
}
}
return null;
}
public int GetTotal(int id)
{
int sum = 0;
if (id == 0)
{//牧师
for (int i = 0; i < 6; i++)
{
if (roles[i] != null && roles[i].GetSign() == 0)
{
sum++;
}
}
}
else if (id == 1)
{//魔鬼
for (int i = 0; i < 6; i++)
{
if (roles[i] != null && roles[i].GetSign() == 1)
{
sum++;
}
}
}
return sum;
}
public void Reset()
{
roles = new RoleModel[6];
}
}
public class BoatModel
{
GameObject boat;//船对象
Vector3[] src_empty_pos;//船在src陆地的两个空位位置
Vector3[] des_empty_pos;//船在des陆地的两个空位位置
public float speed = 15;
GhostBoatActionManager moveController;
Clickable click;
int boat_mark = 1;//船在src为1,在des为-1。
RoleModel[] roles = new RoleModel[2];//船上的两个角色。
public BoatModel()
{//初始化对象
boat = Object.Instantiate(Resources.Load("Prefabs/Boat", typeof(GameObject)), new Vector3(4.5F, 0.5F, 0), Quaternion.identity) as GameObject;
boat.name = "boat";
moveController = (SSDirector.GetInstance().CurrentScenceController as Controller).action_manager;
click = boat.AddComponent(typeof(Clickable)) as Clickable;
src_empty_pos = new Vector3[] { new Vector3(3.8F, 1.1F, 0), new Vector3(5.2F, 1.1F, 0) };
des_empty_pos = new Vector3[] { new Vector3(-5.2F, 1.1F, 0), new Vector3(-3.8F, 1.1F, 0) };
}
public int Total()
{
int sum = 0;
for (int i = 0; i < 2; i++)
{
if (roles[i] != null)
{
sum++;
}
}
return sum;
}
public void Move()//移动船只的同时,调用角色的函数Move,同时将角色移到指定位置。
{
if (boat_mark == -1)
{
moveController.moveBoat(boat, new Vector3(4.5F, 0.5F, 0), speed);
for (int i = 0; i < 2; i++)
{
if (roles[i] != null)
{
roles[i].Move(src_empty_pos[i]);
}
}
boat_mark = 1;
}
else
{
moveController.moveBoat(boat, new Vector3(-4.5F, 0.5F, 0), speed);
for (int i = 0; i < 2; i++)
{
if (roles[i] != null)
{
roles[i].Move(des_empty_pos[i]);
}
}
boat_mark = -1;
}
}
public int GetBoatMark()
{
return boat_mark;
}
public Vector3 GetEmptyPosition()
{//找到当前空位置
if (boat_mark == 1)
{
int pos = -1;
for (int i = 0; i < 2; i++)
{
if (roles[i] == null)
{
pos = i;
break;
}
}
return src_empty_pos[pos];
}
else
{
int pos = -1;
for (int i = 0; i < 2; i++)
{
if (roles[i] == null)
{
pos = i;
break;
}
}
return des_empty_pos[pos];
}
}
public void AddRole(RoleModel role)
{//添加role
for (int i = 0; i < 2; i++)
{
if (roles[i] == null)
{
roles[i] = role;
break;
}
}
}
public RoleModel RemoveRole(string name)
{//删除role
for (int i = 0; i < 2; i++)
{
if (roles[i] != null && roles[i].GetName() == name)
{
roles[i] = null;
return roles[i];
}
}
return null;
}
public void Reset()
{
if (boat_mark == -1)
{
moveController.moveBoat(boat, new Vector3(4.5F, 0.5F, 0), speed);
boat_mark = 1;
}
roles = new RoleModel[2];
}
}
}
界面设计
点击脚本
可点击对象的脚本,用于处理游戏中的角色和船只的点击事件。
IUserAction action;
定义了一个用户操作接口变量action
,用于调用游戏控制器中的方法。RoleModel role = null;
定义了一个角色模型变量role
,用于存储当前点击对象的角色。public void SetRole(RoleModel role)
是一个公共方法,用于设置当前点击对象的角色。private void Start()
是一个启动方法,用于获取游戏控制器对象并赋值给action
变量。private void OnMouseDown()
是一个鼠标点击事件方法,在点击发生时调用。通过比较点击对象的名称来区分是船只还是角色。- 如果点击对象的名称是 "boat",则调用
action.MoveBoat()
方法,移动船只。 - 否则,调用
action.MoveRole(role)
方法,移动角色。角色信息存储在role
变量中,通过SetRole()
方法进行设置
- 如果点击对象的名称是 "boat",则调用
public class Clickable : MonoBehaviour
{
IUserAction action;
RoleModel role = null;
public void SetRole(RoleModel role)
{
this.role = role;
}
private void Start()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;//获取控制器
}
private void OnMouseDown()//点击时调用控制器中的相关函数,用gameObject的name来区分对象。
{
if (gameObject.name == "boat")
{
action.MoveBoat();
}
else
{
action.MoveRole(role);
}
}
}
用户界面的脚本
用户界面的脚本,用于显示游戏状态和处理重新开始游戏的逻辑。
private IUserAction action;
定义了一个用户操作接口变量action
,用于调用游戏控制器中的方法。public int status = 0;
定义了一个整型变量status
,表示游戏的状态,默认值为 0。bool isShow = false;
定义了一个布尔变量isShow
,用于控制界面的显示状态,默认为 false。GUIStyle white_style = new GUIStyle();
定义了一个GUIStyle
变量white_style
,用于设置白色字体样式。GUIStyle black_style = new GUIStyle();
定义了一个GUIStyle
变量black_style
,用于设置黑色字体样式。GUIStyle title_style = new GUIStyle();
定义了一个GUIStyle
变量title_style
,用于设置标题字体样式。
在 Start()
方法中,首先获取当前场景的控制器对象并赋值给 action
变量。然后,根据需要设置不同样式的字体,包括白色字体、黑色字体和标题字体。
在 OnGUI()
方法中,根据游戏的状态来显示相应的界面。如果游戏状态为 -1,即失败状态,显示 "You Lose!" 的黑色字体和一个重新开始按钮。点击重新开始按钮时,调用游戏控制器的 Restart()
方法重新开始游戏,并将 status
设置为 0。如果游戏状态为 1,即胜利状态,显示 "You Win!" 的黑色字体和一个重新开始按钮。点击重新开始按钮时,同样调用游戏控制器的 Restart()
方法重新开始游戏,并将 status
设置为 0。
public class UserGUI : MonoBehaviour
{
private IUserAction action;
public int status = 0;
bool isShow = false;
GUIStyle white_style = new GUIStyle();
GUIStyle black_style = new GUIStyle();
GUIStyle title_style = new GUIStyle();
void Start()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
//字体初始化
white_style.normal.textColor = Color.white;
white_style.fontSize = 20;
black_style.normal.textColor = Color.black;
black_style.fontSize = 30;
title_style.normal.textColor = Color.black;
title_style.fontSize = 45;
}
void OnGUI()
{
if (status == -1)
{
GUI.Label(new Rect(Screen.width / 2 - 60, 180, 100, 30), "You Lose!", black_style);
if (GUI.Button(new Rect(Screen.width / 2 - 40, 240, 100, 30), "Restart"))
{
action.Restart();
status = 0;
}
}
else if (status == 1)
{
GUI.Label(new Rect(Screen.width / 2 - 62, 180, 100, 30), "You Win!", black_style);
if (GUI.Button(new Rect(Screen.width / 2 - 40, 240, 100, 30), "Restart"))
{
action.Restart();
status = 0;
}
}
}
}