Unity3D游戏编程-牧师与恶魔 动作分离版

Unity3D游戏编程-牧师与恶魔 动作分离版

作业要求

牧师与魔鬼的动作分离版。
设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。


项目配置

Windows 10
Unity 2020.3.17f1c1

项目演示

视频演示

点击此处可以前往

项目下载

下载Assets文件夹
点击此处可以前往gitee

文字说明

  1. 创建unity专案后,将保存的文件夹中的Assets替换成在上面项目下载的Assets文件夹
  2. 打开专案,然后点选Assets中的ass加载场景
  3. 运行即可开始游戏
  4. 白色长方体为牧师,红色圆球为恶魔

项目截图

因为实际游玩效果与上一次作业相近,因此不再在此放出截图,可以参考上次作业的博客:
点击此处前往


实现过程和方法(算法)

裁判类Judge

裁判类实际上是把原来FirstController中的check_game_over单独分离出来成为一个类,而check_game_over中主要需要这三个对象的属性来进行判断:

    CoastController fromCoast;
	CoastController toCoast;
	BoatController boat;

因此在Judge类中就有这三个私有对象,以及类的构造函数:

    private CoastController fromCoast;
	private CoastController toCoast;
	private BoatController boat;

    public Judge(CoastController cfrom, CoastController cto, BoatController b){
        fromCoast = cfrom;
        toCoast = cto;
        boat = b;
    } 

在FirstController中就创建Judge类对象,并且进行初始化:

    judge= new Judge(fromCoast, toCoast, boat);

而check_game_over函数就与原来的没有太大差异,FirstController中就可以调用judge.check_game_over()来获取游戏状态。

遇到的问题

在编写judge这个类的时候,参考了其他类的构造而继承了MonoBehaviour,但是编译会报警告说MonoBehaviour的类不可以用new创建,于是judge类直接作为单独的类不继承。


动作分离

动作分离的目的是将运动的共性提取出来,用一个管理器统一管理,提高代码的复用性和可读性。而在此游戏项目中,就是要去掉baseCode中Moveable这个类,而选用动作管理器来实现。

为了更好的理解,从FristController开始分析。FristController中创建了FirstSceneActionManager这个类的对象,如下:

private FirstSceneActionManager actionManager;

于是观察FirstSceneActionManager:

FirstSceneActionManager
    public class FirstSceneActionManager:ActionManager {
        public void moveBoat(BoatController boat) {
		    MoveToAction action = MoveToAction.getAction(boat.getDestination(), boat.movingSpeed);
		    this.addAction(boat.getGameobj(), action, this);
        }

        public void moveCharacter(MyCharacterController characterCtrl, Vector3 destination) {
			Vector3 currentPos = characterCtrl.getPos();
			Vector3 middlePos = currentPos;
			if (destination.y > currentPos.y) {		//from low(boat) to high(coast)
				middlePos.y = destination.y;
			} else {	//from high(coast) to low(boat)
				middlePos.x = destination.x;
			}
			ObjAction action1 = MoveToAction.getAction(middlePos, characterCtrl.movingSpeed);
			ObjAction action2 = MoveToAction.getAction(destination, characterCtrl.movingSpeed);
			ObjAction seqAction = SequenceAction.getAction(1, 0, new List<ObjAction>{action1, action2});
			this.addAction(characterCtrl.getGameobj(), seqAction, this);
        }
    }

注:moveBoat()中boat.getDestination()、boat.getGameobj()是BaseCode中BoatController的函数,前者作用返回船目前所在岸的对岸(目的地),后者是返回这个船对象本身。
moveCharacter()中characterCtrl.getPos()、characterCtrl.getGameobj()类似,是BaseCode中MyCharacterController的函数,前者返回角色当前位置,后者返回角色对象本身。

FirstController就是通过FirstSceneActionManager来管理所有动作,可以看到它里面包括了:

  1. 移动船只moveBoat,调用MoveToAction.getAction()对动作初始化,MoveToAction是让一个对象直线移动的动作类:
     public static MoveToAction getAction(Vector3 target, float speed) {
            MoveToAction action = ScriptableObject.CreateInstance<MoveToAction>();
            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.whoToNotify.actionDone(this);
           }
       }

个人感觉比如说动作A,它的类是MoveToAction,而动作A就要用getAction来进行初始化构造(目的地,速度),实际上在界面上的移动就依赖updata中MoveTowards来完成。

初始化了动作之后,利用this.addAction()执行动作,其参数就是(执行动作的对象,执行什么动作,通知哪个类我的动作已完成)。
因为MoveToAction是一次性动作,在updata的最后会使用this.whoToNotify.actionDone(this)告知动作已完成,这个动作就会被系统回收。

  1. 移动角色,因为角色如果是上船会需要两段移动,所以用了两次MoveToAction作为组合动作seqAction。组合动作利用SequenceAction.getAction()进行初始化,参数为(循环的次数,从哪一个动作开始做起,动作列表),Start()是将目前所轮到的动作的参数进行赋值,然后利用this.addAction()执行目前所轮到的动作。而在最后,需要逐一为列表中的动作都执行actionDone()。
        public static SequenceAction getAction(int repeat, int currentActionIndex, List<ObjAction> sequence) {
            SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
            action.sequence = sequence;
            action.repeat = repeat;
            action.currentActionIndex = currentActionIndex;
            return action;
        }
        
		public override void Start() {
            foreach(ObjAction action in sequence) {
                action.gameObject = this.gameObject;
                action.transform = this.transform;
                action.whoToNotify = this;
                action.Start();
            }
        }
        
        public override void Update() {
            if (sequence.Count == 0)return;
            if (currentActionIndex < sequence.Count) {
                sequence[currentActionIndex].Update();
            }
        }

        public void actionDone(ObjAction source) {
            source.destroy = false;
            this.currentActionIndex++;
            if (this.currentActionIndex >= sequence.Count) {
                this.currentActionIndex = 0;
                if (repeat > 0) repeat--;
                if (repeat == 0) {
                    this.destroy = true;
                    this.whoToNotify.actionDone(this);
                }
            }
        }
        

FirstSceneActionManager继承了ActionManager,addAction()是在ActionManager中实现。FirstSceneActionManage的作用是更好地让FirstController简洁地进行调用ActionManager函数。

ActionManager与ObjAction

ActionManager是动作对象管理器的基类,实现了所有动作的基本管理。而ObjAction是所有动作的基类,ActionManager就是通过ObjAction这个接口来管理动作的。于是有关系ObjAction > ActionManager > FirstSceneActionManager。
ActionManager与ObjAction这两个类可以作为模板,基本固定。

ActionCallback

ActionCallback是一个接口,实现了这个接口的类,就可以知道到“某个动作已完成”(动作一完成actionDone方法就会被调用),并对这个事件做出反应。

    public interface ActionCallback {
        void actionDone(ObjAction source);
    }

SequenceAction、ActionManager中就有actionDone()这个类的实现。


然后就可以在FirstController中调用FirstSceneActionManager的moveBoat和moveCharacter实现船只和角色移动了:
两条语句注释的是前一个作业利用moveable类的表示形式,下面是这次作业用actionManager的表示形式

//characterCtrl.moveToPosition (whichCoast.getEmptyPosition ());
actionManager.moveCharacter(characterCtrl, whichCoast.getEmptyPosition ());

//boat.Move ();
actionManager.moveBoat(boat);

并且经过对比更容易看懂这张图(动作管理器的设计):
请添加图片描述

参考资料

1.学习Unity(7)小游戏架构改进——实现动作管理器

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是C#语言实现牧师与野人问题求解的代码和注释: ```csharp using System; using System.Collections.Generic; namespace MissionariesAndCannibals { class Program { static void Main(string[] args) { // 初始化状态 State initialState = new State(3, 3, true, 0, 0); // 初始化搜索树 SearchTree searchTree = new SearchTree(initialState); // 进行搜索 searchTree.BFS(); } } // 表示状态的类 class State { public int MissionariesLeft { get; set; } // 左岸传教士数量 public int CannibalsLeft { get; set; } // 左岸野人数量 public bool BoatPosition { get; set; } // 船的位置,true表示在左岸,false表示在右岸 public int MissionariesRight { get; set; } // 右岸传教士数量 public int CannibalsRight { get; set; } // 右岸野人数量 // 构造函数 public State(int missionariesLeft, int cannibalsLeft, bool boatPosition, int missionariesRight, int cannibalsRight) { MissionariesLeft = missionariesLeft; CannibalsLeft = cannibalsLeft; BoatPosition = boatPosition; MissionariesRight = missionariesRight; CannibalsRight = cannibalsRight; } // 判断状态是否合法 public bool IsValid() { if (MissionariesLeft < 0 || CannibalsLeft < 0 || MissionariesRight < 0 || CannibalsRight < 0) { return false; } if (MissionariesLeft > 3 || CannibalsLeft > 3 || MissionariesRight > 3 || CannibalsRight > 3) { return false; } if (CannibalsLeft > MissionariesLeft && MissionariesLeft > 0) { return false; } if (CannibalsRight > MissionariesRight && MissionariesRight > 0) { return false; } return true; } // 判断状态是否为目标状态 public bool IsGoal() { return MissionariesLeft == 0 && CannibalsLeft == 0; } // 判断两个状态是否相等 public override bool Equals(object obj) { State state = obj as State; if (state == null) { return false; } return MissionariesLeft == state.MissionariesLeft && CannibalsLeft == state.CannibalsLeft && BoatPosition == state.BoatPosition && MissionariesRight == state.MissionariesRight && CannibalsRight == state.CannibalsRight; } // 获取状态的哈希值 public override int GetHashCode() { return MissionariesLeft * 10000 + CannibalsLeft * 1000 + (BoatPosition ? 100 : 0) + MissionariesRight * 10 + CannibalsRight; } // 获取状态的字符串表示 public override string ToString() { return "MissionariesLeft: " + MissionariesLeft + ", CannibalsLeft: " + CannibalsLeft + ", BoatPosition: " + (BoatPosition ? "Left" : "Right") + ", MissionariesRight: " + MissionariesRight + ", CannibalsRight: " + CannibalsRight; } } // 表示搜索树的类 class SearchTree { private State _initialState; // 初始状态 private Queue<Node> _frontier; // 存放待扩展的节点的队列 private HashSet<State> _exploredSet; // 存放已扩展过的状态的集合 // 构造函数 public SearchTree(State initialState) { _initialState = initialState; _frontier = new Queue<Node>(); _frontier.Enqueue(new Node(_initialState, null)); _exploredSet = new HashSet<State>(); } // 广度优先搜索 public void BFS() { while (_frontier.Count > 0) { Node node = _frontier.Dequeue(); State state = node.State; if (state.IsGoal()) { // 找到了目标状态,输出路径 List<State> path = new List<State>(); while (node != null) { path.Insert(0, node.State); node = node.Parent; } foreach (State s in path) { Console.WriteLine(s); } return; } _exploredSet.Add(state); List<State> successors = GetSuccessors(state); foreach (State successor in successors) { if (!_exploredSet.Contains(successor)) { _frontier.Enqueue(new Node(successor, node)); } } } Console.WriteLine("No solution found."); } // 获取一个状态的所有合法后继状态 private List<State> GetSuccessors(State state) { List<State> successors = new List<State>(); if (state.BoatPosition) { // 船在左岸 for (int i = 0; i <= 2; i++) { for (int j = 0; j <= 2; j++) { if (i + j >= 1 && i + j <= 2) { State successor = new State(state.MissionariesLeft - i, state.CannibalsLeft - j, false, state.MissionariesRight + i, state.CannibalsRight + j); if (successor.IsValid()) { successors.Add(successor); } } } } } else { // 船在右岸 for (int i = 0; i <= 2; i++) { for (int j = 0; j <= 2; j++) { if (i + j >= 1 && i + j <= 2) { State successor = new State(state.MissionariesLeft + i, state.CannibalsLeft + j, true, state.MissionariesRight - i, state.CannibalsRight - j); if (successor.IsValid()) { successors.Add(successor); } } } } } return successors; } } // 表示搜索树中的节点的类 class Node { public State State { get; set; } // 节点对应的状态 public Node Parent { get; set; } // 父节点 // 构造函数 public Node(State state, Node parent) { State = state; Parent = parent; } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值