游戏设计
游戏简介
规则
初始左岸有三名牧师和三名魔鬼,一只船靠着左岸;
船最多载两人,至少载一人才能到对岸;
如果某岸上牧师数小于魔鬼数,牧师会遇害,游戏失败;
牧师和魔鬼都到右岸,则游戏胜利;
游戏启动准备:
脚本UserGUI挂到Main Camera上,FirstSceneController、CCActionManager和Judge挂到一个空对象上。
玩法:
运行后,玩家可以点击人物(牧师或魔鬼)以及船,指挥人物、船等游戏对象自行根据当前条件完成上下船和划船动作。
动作分离版UML图:
其中蓝色部分为动作管理器的原理实现,负责游戏场景的对象的动作管理;绿色部分为裁判类,用于判断游戏是否结束并通知场景控制器。
一个场景可以有多个动作控制器CCActionManager,负责管理不同类的动作,每个动作控制器都充当其基类SSActionManager的门面类,提供可调用的方法方便场景控制器发命令,负责判断动作是否可行以及规定动作的详细信息,再由SSActionManager处理。
动作队列在SSActionManager中进行处理,动作分为组合动作(CCSequenceAction)和单个动作(CCMoveToAction),处理的方法就是修改动作Action的属性并调用它的Update方法,即给动作开个头让它自己运行,而动作结束后会调用回调函数SSActionEvent(),可以在这个函数中加入运动状态的恢复、游戏结果判断等。
裁判类Judge:一个裁判对应一个(或多个)动作控制器,由与动作控制器协作保证同一时间只有一套动作运行,而checkGame()方法对场景中的情况进行分析,判断游戏是否已经成功或失败,如果是,就发送信息给场景控制器。
上一版本——mvc模式
上一版本将动作管理和判断都放在场景控制器中,且使用按钮界面,场景控制器和用户界面的脚本都比较冗长,不方便调试修改和复用。
游戏演示
动作分离版牧师与魔鬼游戏演示
上面视频中分别演示了失败情况和成功情况。(可能是录屏的原因,鼠标位置似乎偏上了,实际我鼠标是对准船了的)
游戏组成
预制体
材质
一切从简,只换了颜色
代码文件
游戏代码
场景控制器FirstController
加载预制体,监听裁判类信息,实现比按钮方便很多的鼠标点击选择对象功能,若点击人物(用了名字来标识,固定位置),根据人物位置进行上下船方法调用;若点击船,调用划船方法。并实现用户界面接口方法。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
public CCActionManager actionManager { get; set;}
public Judge judge { get; set; }
public GameObject boat_p1=null, boat_p2 = null;
public List<GameObject> leftObjects=new List<GameObject>(), rightObjects=new List<GameObject>();
public GameObject boat,scene_obj;
private string judgeMessage=null;
// the first scripts
void Awake () {
SSDirector director = SSDirector.getInstance ();
director.setFPS (60);
director.currentSceneController = this;
director.currentSceneController.LoadResources ();
Debug.Log ("awake FirstController!");
}
// listen to the Judge
public void getJudgeMessage(string msg)
{
this.judgeMessage = msg;
}
// loading resources for first scence
public void LoadResources () {
scene_obj = Instantiate<GameObject>(
Resources.Load<GameObject>("prefabs/scene_obj"),
Vector3.zero, Quaternion.identity);
boat = Instantiate<GameObject>(
Resources.Load<GameObject>("prefabs/boat"),
Vector3.zero, Quaternion.identity);
boat.transform.position = new Vector3 (3,0,0);
boat.name = "boat";
GameObject obj;
for(int i = 1;i <= 3;i++)
{
obj = Instantiate<GameObject>(
Resources.Load<GameObject>("prefabs/priest"),
new Vector3(i*2+5,4,0), Quaternion.identity);
obj.name = i.ToString();
leftObjects.Add(obj);
}
for (int i = 4; i <= 6; i++)
{
obj = Instantiate<GameObject>(
Resources.Load<GameObject>("prefabs/devil"),
new Vector3( i*2+5, 4, 0), Quaternion.identity);
obj.name = i.ToString();
leftObjects.Add(obj);
}
;
}
private void MoveBoat()
{
actionManager.BoatMove();
}
private void MovePeople(GameObject obj)
{
if (obj.transform.position.x>5|| obj.transform.position.x < -5)
{
actionManager.MoveToBoat(obj);
}
else
{
actionManager.LeaveBoat(obj);
}
}
#region IUserAction implementation
public void GameOver ()
{
SSDirector.getInstance ().NextScene ();
}
public string GameOut()
{
// 停止游戏
if(judgeMessage!=null)SSDirector.getInstance ().setFPS(0);
return judgeMessage;
}
public void Click()
{
GameObject gameObj = null;
if (Input.GetMouseButtonDown(0))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) gameObj = hit.transform.gameObject;
}
if (gameObj == null) return;
else if (gameObj.name == "1" || gameObj.name == "2" || gameObj.name == "3"
|| gameObj.name == "4" || gameObj.name == "5" || gameObj.name == "6")
{
Debug.Log(gameObj.name);
MovePeople(gameObj);
}
else if (gameObj.name == "boat") MoveBoat();
}
#endregion
// Use this for initialization
void Start () {
//give advice first
}
// Update is called once per frame
void Update () {
//give advice first
}
}
实现的接口:
using System;
public interface IUserAction
{
void GameOver();
string GameOut();
void Click();
}
using System;
public interface ISceneController
{
void LoadResources();
}
用户GUI界面UserGUI
调用IUserAction方法,实现点击功能、游戏结束提示
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour {
private IUserAction action;
private string gameMessage;
private GUIStyle style;
void Start () {
action = SSDirector.getInstance ().currentSceneController as IUserAction;
}
void OnGUI()
{
float width = Screen.width / 8;
float height = Screen.height / 12;
if (GUI.Button(new Rect(0, 0, width, height), "结束游戏"))
{
action.GameOver();
}
if (action.GameOut() != null)
{
GUI.Box(new Rect(width * 4, height * 5, width, height), action.GameOut());
}
}
void Update()
{
action.Click();
}
}
导演类SSDirector
实现场景切换、场景停止、时间流速设置
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object {
// singlton instance
private static SSDirector _instance;
public ISceneController currentSceneController { get; set;}
public bool running{ get; set;}
// get instance anytime anywhare!
public static SSDirector getInstance() {
if (_instance == null) {
_instance = new SSDirector ();
}
return _instance;
}
public int getFPS() {
return Application.targetFrameRate;
}
public void setFPS(int fps) {
Application.targetFrameRate = fps;
}
public void NextScene(){
Debug.Log ("Waiting next Scene now...");
Application.Quit ();
}
}
动作管理器部分
动作类(抽象类)SSAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSAction : ScriptableObject {
public bool enable = true;
public bool destory = false;
public GameObject gameobject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }
protected SSAction () {}
// Use this for initialization
public virtual void Start () {
throw new System.NotImplementedException ();
}
// Update is called once per frame
public virtual void Update () {
throw new System.NotImplementedException ();
}
}
单动作类CCMoveToAction
GetSSAction方法在动作管理器CCActionManager中调用,获取动作信息(目标向量和速度)。Update中进行动作实现。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCMoveToAction : SSAction
{
public Vector3 target;
public float speed;
public static CCMoveToAction GetSSAction(Vector3 target, float speed){
CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction> ();
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) {
//waiting for destroy
this.destory = true;
this.callback.SSActionEvent (this);
}
}
public override void Start () {
}
}
组合动作类CCSequenceAction
一组动作,repeat代表重复次数,-1则不断重复。start即动作列表的索引,Upadate方法中调用单个动作的Update来逐步完成动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCSequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -1; //repeat forever
public int start = 0;
public static CCSequenceAction GetSSAction(int repeat, int start , List<SSAction> sequence){
CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction> ();
action.repeat = repeat;
action.sequence= sequence;
action.start = start;
return action;
}
// Update is called once per frame
public override void Update ()
{
if (sequence.Count == 0) return;
if (start < sequence.Count) {
sequence [start].Update ();
}
}
public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
{
source.destory = false;
this.start++;
if (this.start >= sequence.Count) {
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0) { this.destory = true; this.callback.SSActionEvent (this); }
}
}
// Use this for initialization
public override void Start () {
foreach (SSAction action in sequence) {
action.gameobject = this.gameobject;
action.transform = this.transform;
action.callback = this;
action.Start ();
}
}
void OnDestory() {
//TODO: something
Destroy(this);
}
}
动作控制器基类SSActionManager
RunAction方法将待执行动作加入执行队列中,在Update中执行,执行过的动作加入销毁队列销毁。不过游戏要求同一时间只有一个动作,所以用了裁判类判断来限制了动作的生成和积累。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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> ();
// Update is called once per frame
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.destory) {
waitingDelete.Add(ac.GetInstanceID()); // release action
} else if (ac.enable) {
ac.Update (); // update action
}
}
foreach (int key in waitingDelete) {
SSAction ac = actions[key];
actions.Remove(key);
Object.Destroy(ac);
}
waitingDelete.Clear ();
}
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) {
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add (action);
action.Start ();
}
// Use this for initialization
protected void Start () {
}
}
动作控制器类CCActionManager
start中和场景控制器配对,并实现划船、上船下船的方法,既生成动作,还要在动作生成之前判断条件是否合适。实现了回调函数SSActionEvent,在动作结束后会更新场景中动作状态并检查游戏是否结束。同时不断调用基类的Update及时处理动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback {
private FirstController sceneController;
private CCMoveToAction moveToA , moveToB, moveToC, moveToD,moveToE,moveToF;
public void BoatMove()
{
//检查是否在做动作
if (sceneController.judge.checkMove()) return;
//检查船上是否有人
if (sceneController.boat == null || (sceneController.boat_p1 == null && sceneController.boat_p2 == null)) return;
float next_x = sceneController.boat.transform.position.x*-1;
// 船、船上的人的下一个位置和移动速度
moveToA = CCMoveToAction.GetSSAction(new Vector3(next_x, 0, 0), 6);
moveToB = CCMoveToAction.GetSSAction(new Vector3(next_x - 1, 3, 0), 6);
moveToC = CCMoveToAction.GetSSAction(new Vector3(next_x + 1, 3, 0), 6);
// boat moves
this.RunAction(sceneController.boat, moveToA, this);
// set ismoving
sceneController.judge.setIsMoving(true);
// boat_p move
if (sceneController.boat_p1 !=null)
{
GameObject p = sceneController.boat_p1;
float x = p.transform.position.x;
if (x > 0) sceneController.leftObjects.Remove(p);
else sceneController.rightObjects.Remove(p);
if (x < 0) sceneController.leftObjects.Add(p);
else sceneController.rightObjects.Add(p);
this.RunAction(sceneController.boat_p1 , moveToB, this);
}
if (sceneController.boat_p2 != null)
{
GameObject p = sceneController.boat_p2;
float x = p.transform.position.x;
if (x > 0) sceneController.leftObjects.Remove(p);
else sceneController.rightObjects.Remove(p);
if (x < 0) sceneController.leftObjects.Add(p);
else sceneController.rightObjects.Add(p);
this.RunAction(sceneController.boat_p2, moveToC, this);
}
}
public void MoveToBoat(GameObject obj)
{
//检查是否在做动作
if (sceneController.judge.checkMove()) return;
int j = sceneController.boat_p1 == null ? 1 : (sceneController.boat_p2 == null ? 2 : 0);
if (j == 0) return;
else if(j==1) sceneController.boat_p1 = obj;
else sceneController.boat_p2= obj;
float next_x = sceneController.boat.transform.position.x-3+j*2;
// 按上-左/右-下的顺序平移
moveToD = CCMoveToAction.GetSSAction(new Vector3(obj.transform.position.x, 9, 0), 6);
moveToE = CCMoveToAction.GetSSAction(new Vector3(next_x, 9, 0), 6);
moveToF = CCMoveToAction.GetSSAction(new Vector3(next_x, 3, 0), 6);
CCSequenceAction ccs = CCSequenceAction.GetSSAction(1, 0, new List<SSAction> { moveToD, moveToE,moveToF });
this.RunAction(obj, ccs, this);
// set ismoving
sceneController.judge.setIsMoving(true);
}
public void LeaveBoat(GameObject obj)
{
//检查是否在做动作
if (sceneController.judge.checkMove()) return;
if (obj == sceneController.boat_p1) sceneController.boat_p1=null;
else if(obj == sceneController.boat_p2) sceneController.boat_p2=null;
int i = int.Parse(obj.name);
float next_x = (i * 2 + 5)*(sceneController.boat.transform.position.x>0?1:-1);
// 按上-左/右-下的顺序平移
moveToD = CCMoveToAction.GetSSAction(new Vector3(obj.transform.position.x, 9, 0), 5);
moveToE = CCMoveToAction.GetSSAction(new Vector3(next_x, 9, 0), 5);
moveToF = CCMoveToAction.GetSSAction(new Vector3(next_x, 4, 0), 5);
CCSequenceAction ccs = CCSequenceAction.GetSSAction(1, 0, new List<SSAction> { moveToD, moveToE, moveToF });
this.RunAction(obj, ccs, this);
// set ismoving
sceneController.judge.setIsMoving(true);
}
protected new void Start() {
sceneController = (FirstController)SSDirector.getInstance ().currentSceneController;
sceneController.actionManager = this;
}
// Update is called once per frame
protected new void Update ()
{
base.Update ();
}
#region ISSActionCallback implementation
public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
{
Debug.Log("finishmove");
// 一个操作的动作结束,可以进行下一个操作
sceneController.judge.setIsMoving(false);
// 检查游戏是否结束
sceneController.judge.checkGame();
Destroy(source);
}
#endregion
}
实现的接口ISSActionCallback:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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);
}
裁判类
start中和场景控制器配对。通过获取场景控制器信息来判断游戏情况并通知场景控制器。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Judge : MonoBehaviour
{
private bool move=false;
private FirstController sceneController;
public void checkGame()
{
//检查游戏成功
if(sceneController.rightObjects.Count == 6)
{
sceneController.getJudgeMessage("You Win!");
}
//检查游戏失败
//左岸
int p=0, d=0;
for(int i=0;i<sceneController.leftObjects.Count;i++)
{
if (sceneController.leftObjects[i].name=="1"
|| sceneController.leftObjects[i].name == "2"
|| sceneController.leftObjects[i].name == "3")
{
p++;
}
else
{
d++;
}
}
if (p < d && p!=0) { Debug.Log(p); Debug.Log(d); sceneController.getJudgeMessage("You Lose!"); }
//右岸
p = 0; d = 0;
for (int i = 0; i < sceneController.rightObjects.Count; i++)
{
if (sceneController.rightObjects[i].name == "1"
|| sceneController.rightObjects[i].name == "2"
|| sceneController.rightObjects[i].name == "3")
{
p++;
}
else
{
d++;
}
}
if (p < d && p != 0) { Debug.Log(p); Debug.Log(d); sceneController.getJudgeMessage("You Lose!"); }
}
public void setIsMoving(bool move)
{
this.move = move;
}
public bool checkMove()
{
return this.move;
}
// Start is called before the first frame update
void Start()
{
sceneController = SSDirector.getInstance().currentSceneController as FirstController;
sceneController.judge = this;
}
// Update is called once per frame
void Update()
{
}
}