一、游戏要求
提交要求:
游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标; 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
程序设计要求:
- 必须使用订阅与发布模式传消息
- subject:OnLostGoal
- Publisher: ?
- Subscriber: ?
- 工厂模式生产巡逻兵
友善提示1:
- 生成 3~5个边的凸多边型
- 随机生成矩形
- 在矩形每个边上随机找点,可得到 3 - 4 的凸多边型
- 5 ?
友善提示2:
- 参考以前博客,给出自己新玩法
二、UML图
根据发布者接收者模式,我设计的程序UML图如下:
在UML图形中,GameEventManager是发布者,GameStatus是信息的接收者。而在SSActionManager这部分中,主要是实现对动作的管理。在PatrolBehavior中是实现巡逻兵的动作交互的。
接下来是重点程序的举例。
三、程序源码及说明
巡逻兵部分:
1、巡逻兵工厂模式:
巡逻兵工厂模式同时也是单例模式,而且在实现的时候,要生成九个不同位置上的巡逻兵,以及生成巡逻兵实例。具体代码如下:
public class PatrolFactory : System.Object
{
private static PatrolFactory instance;
private GameObject PatrolItem;
//九个方格中的巡逻兵初始位置
private Vector3[] PatrolPos = new Vector3[] {new Vector3(9, 0, 9), new Vector3(0, 0, 9),
new Vector3(-9, 0, 9), new Vector3(0, 0, 0), new Vector3(9, 0, 0), new Vector3(-9, 0, 0),
new Vector3(9, 0, -9), new Vector3(0, 0, -9), new Vector3(-9, 0, -9)};
public static PatrolFactory getInstance()
{
if (instance == null)
instance = new PatrolFactory();
return instance;
}
public void initItem(GameObject _PatrolItem)
{
PatrolItem = _PatrolItem;
}
public GameObject getPatrol()
{
GameObject newPatrol = Camera.Instantiate(PatrolItem);
return newPatrol;
}
public Vector3[] getPosSet()
{
return PatrolPos;
}
}
2、巡逻兵动作管理:
巡逻兵的本身的动作主要有两个,一个是当游戏玩家Hero进入当前巡逻兵的范围的时候,要变化本身的运动轨迹;另外一个动作是当游戏玩家Hero与巡逻兵相撞的时候,判定游戏结束。具体代码如下:
public class PatrolBehavior : MonoBehaviour {
private IAddAction addAction;
private IGameStatusOp gameStatus;
public int index;
public bool isCatching;
private float CATCH_RADIUS = 3.0f;
// Use this for initialization
void Start () {
addAction = SceneController.getInstance() as IAddAction;
gameStatus = SceneController.getInstance() as IGameStatusOp;
index = getOwnIndex();
isCatching = false;
}
// Update is called once per frame
void Update () {
checkNearByHero();
}
int getOwnIndex()
{
string name = this.gameObject.name;
char cindex = name[name.Length - 1];
int result = cindex - '0';
return result;
}
//检测进入自己区域的hero
void checkNearByHero()
{
if (gameStatus.getHeroStandOnArea() == index)
{ //只有当走进自己的区域
if (!isCatching)
{
isCatching = true;
addAction.addDirectMovement(this.gameObject);
}
}
else
{
if (isCatching)
{ //刚才为捕捉状态,但此时hero已经走出所属区域
gameStatus.heroEscapeAndScore();
isCatching = false;
addAction.addRandomMovement(this.gameObject, false);
}
}
}
void OnCollisionStay(Collision e)
{
//撞击围栏,选择下一个点移动
if (e.gameObject.name.Contains("Patrol") || e.gameObject.name.Contains("fence")
|| e.gameObject.tag.Contains("FenceAround"))
{
isCatching = false;
addAction.addRandomMovement(this.gameObject, false);
}
//撞击hero,游戏结束
if (e.gameObject.name.Contains("Hero"))
{
gameStatus.patrolHitHeroAndGameover();
Debug.Log("Game Over!");
}
}
}
玩家Player部分
这部分的脚本是放在玩家Hero预制上的,所以主要的动作我设定的主要是,判定玩家所处的区域是哪块就行了。代码如下:
public class HeroBehavior : MonoBehaviour {
public int standOnArea = -1;
void Update () {
modifyStandOnArea();
}
void modifyStandOnArea()
{
float posX = this.gameObject.transform.position.x;
float posZ = this.gameObject.transform.position.z;
if (posZ >= FenchLocation.FenchHoriUp)
{
if (posX < FenchLocation.FenchVertLeft)
standOnArea = 0;
else if (posX > FenchLocation.FenchVertRight)
standOnArea = 2;
else
standOnArea = 1;
}
else if(posZ < FenchLocation.FenchHoriUp && posZ > FenchLocation.FenchHoriDown)
{
if (posX < FenchLocation.FenchVertLeft)
standOnArea = 3;
else if (posX > FenchLocation.FenchVertRight)
standOnArea = 5;
else
standOnArea = 4;
}
else
{
if (posX < FenchLocation.FenchVertLeft)
standOnArea = 6;
else if (posX > FenchLocation.FenchVertRight)
standOnArea = 8;
else
standOnArea = 7;
}
}
}
主体部分
1、SceneController部分
SceneController继承了三个接口类:IUserAction、IAddAction和IGameStatusOp。其中成员对象有SceneController的实例,GameModel和GameEventManager。用于控制游戏中的各类事件:游戏玩家逃离小兵得分,玩家的运动等等。主要代码如下:
public class SceneController : System.Object, IUserAction, IAddAction, IGameStatusOp
{
private static SceneController instance;
private GameModel myGameModel;
private GameEventManager myGameEventManager;
public static SceneController getInstance()
{
if (instance == null)
instance = new SceneController();
return instance;
}
internal void setGameModel(GameModel _myGameModel)
{
if (myGameModel == null)
{
myGameModel = _myGameModel;
}
}
internal void setGameEventManager(GameEventManager _myGameEventManager)
{
if (myGameEventManager == null)
{
myGameEventManager = _myGameEventManager;
}
}
//实现IUserAction接口
public void heroMove(int dir)
{
myGameModel.HeroMove(dir);
}
//实现IAddAction接口
public void addRandomMovement(GameObject sourceObj, bool isActive)
{
myGameModel.addRandomMovement(sourceObj, isActive);
}
public void addDirectMovement(GameObject sourceObj)
{
myGameModel.addDirectMovement(sourceObj);
}
//实现IGameStatusOp接口
public int getHeroStandOnArea()
{
return myGameModel.GetHeroArea();
}
public void heroEscapeAndScore()
{
myGameEventManager.heroEscape();
}
public void patrolHitHeroAndGameover()
{
myGameEventManager.patrolHitHero();
}
}
2、GameModel类
这个类是用来具体实现人物和巡逻兵运动的类。其中定义了游戏中所需要的各类对象和方法。对象包括PatrolItem,HeroItem,sceneController等。方法可以分为几大类:实例化游戏玩家和巡逻兵,实现巡逻兵绕一个凸多边形的边缘运动,实现对象向某一个规定的方向运动,以及游戏玩家的运动等。
具体的函数实现如下:
public class GameModel : SSActionManager, ISSActionCallback {
public GameObject PatrolItem, HeroItem;
private SceneController sceneController;
private GameObject hero, sceneModel, canvas;
private List<GameObject> patrolSet;
private List<int> patrolLastDir;
private const float PERSON_SPEED_NORMAL = 0.05f;
private const float PERSON_SPEED_CATCHING = 0.06f;
void Awake()
{
PatrolFactory.getInstance().initItem(PatrolItem);
}
// Use this for initialization
protected new void Start () {
sceneController = SceneController.getInstance();
sceneController.setGameModel(this);
GenHero();
GenPatrols();
}
// Update is called once per frame
protected new void Update()
{
base.Update();
}
//生产游戏玩家Hero
void GenHero()
{
hero = Instantiate(HeroItem);
}
//生产巡逻兵
void GenPatrols()
{
patrolSet = new List<GameObject>(9);
patrolLastDir = new List<int>(9);
Vector3[] posSet = PatrolFactory.getInstance().getPosSet();
//从巡逻兵工厂得到九个位置,并生产出巡逻兵
for(int i = 0; i < 9; i++)
{
GameObject newPatrol = PatrolFactory.getInstance().getPatrol();
newPatrol.transform.position = posSet[i];
newPatrol.name = "Patrol" + i;
patrolLastDir.Add(-2);
patrolSet.Add(newPatrol);
addRandomMovement(newPatrol, true);
}
}
//实现接口ISSActionCallback
public void SSActionEvent(SSAction source,
SSActionEventType eventType = SSActionEventType.Completed,
SSActionTargetType intParam = SSActionTargetType.Normal, //动作结束时回调,需要告知是哪种动作
string strParam = null,
Object objParam = null)
{
if (intParam == SSActionTargetType.Normal)
addRandomMovement(source.gameObject, true);
else
addDirectMovement(source.gameObject);
}
//巡逻兵沿凸多边形边缘随意方向运动
public void addRandomMovement(GameObject sourceObj, bool isActive)
{
int ind = GetIndex(sourceObj);
int randomDir = GetRandomDirection(ind, isActive);
patrolLastDir[ind] = randomDir;
sourceObj.transform.rotation = Quaternion.Euler(new Vector3(0, randomDir * 90, 0));
Vector3 target = sourceObj.transform.position;
switch (randomDir)
{
case Direction.UP:
target += new Vector3(0, 0, 1);
break;
case Direction.DOWN:
target += new Vector3(0, 0, -1);
break;
case Direction.LEFT:
target += new Vector3(-1, 0, 0);
break;
case Direction.RIGHT:
target += new Vector3(1, 0, 0);
break;
}
AddSingleMove(sourceObj, target, PERSON_SPEED_NORMAL, false);
}
//游戏对象往某一个规定的方向运动
public void addDirectMovement(GameObject sourceObj)
{
int index = GetIndex(sourceObj);
patrolLastDir[index] = -2;
sourceObj.transform.LookAt(sourceObj.transform);
Vector3 oriTarget = hero.transform.position - sourceObj.transform.position;
Vector3 target = new Vector3(oriTarget.x / 4.0f, 0, oriTarget.z / 4.0f);
target += sourceObj.transform.position;
AddSingleMove(sourceObj, target, PERSON_SPEED_CATCHING, true);
}
//获取巡逻兵标号
int GetIndex(GameObject sourceObj)
{
string name = sourceObj.name;
char ind = name[name.Length - 1];
int result = ind - '0';
return result;
}
//获取任意方向
int GetRandomDirection(int ind, bool isActive)
{
int randomDir = Random.Range(-1, 3);
if (!isActive)
{
while(patrolLastDir[ind] == randomDir)
{
randomDir = Random.Range(-1, 3);
}
}
else
{
while (patrolLastDir[ind] == 0 && randomDir == 2
|| patrolLastDir[ind] == 2 && randomDir == 0
|| patrolLastDir[ind] == 1 && randomDir == -1
|| patrolLastDir[ind] == -1 && randomDir == 1
|| PatrolOutOfArea(ind, randomDir))
{
randomDir = Random.Range(-1, 3);
}
}
return randomDir;
}
//判定巡逻兵随意方向的下一步是否或出规定的区域
bool PatrolOutOfArea(int ind, int randomDir)
{
Vector3 patrolPos = patrolSet[ind].transform.position;
float posX = patrolPos.x;
float posZ = patrolPos.z;
switch (ind)
{
case 0:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriUp)
return true;
break;
case 1:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
|| randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriUp)
return true;
break;
case 2:
if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriUp)
return true;
break;
case 3:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriUp
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriDown)
return true;
break;
case 4:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
|| randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriUp
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriDown)
return true;
break;
case 5:
if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriUp
|| randomDir == 2 && posZ - 1 < FenchLocation.FenchHoriDown)
return true;
break;
case 6:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertLeft
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriDown)
return true;
break;
case 7:
if (randomDir == 1 && posX + 1 > FenchLocation.FenchVertRight
|| randomDir == -1 && posX - 1 < FenchLocation.FenchVertLeft
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriDown)
return true;
break;
case 8:
if (randomDir == -1 && posX - 1 < FenchLocation.FenchVertRight
|| randomDir == 0 && posZ + 1 > FenchLocation.FenchHoriDown)
return true;
break;
}
return false;
}
//对某一个确定的对象完成一步的运动
void AddSingleMove(GameObject sourceObj, Vector3 target, float speed, bool isCatching)
{
this.runAction(sourceObj, CCMoveToAction.CreateSSAction(target, speed, isCatching), this);
}
//给某一个特定的对象完成某一系列的运动
void AddCombinedMoving(GameObject sourceObj, Vector3[] target, float[] speed, bool isCatching)
{
List<SSAction> action = new List<SSAction>();
for(int i = 0; i < target.Length; i++)
{
action.Add(CCMoveToAction.CreateSSAction(target[i], speed[i], isCatching));
}
CCSequeneActions moveSeq = CCSequeneActions.CreateSSAction(action);
this.runAction(sourceObj, moveSeq, this);
}
//游戏玩家运动
public void HeroMove(int dir)
{
hero.transform.rotation = Quaternion.Euler(new Vector3(0, dir * 90, 0));
switch (dir)
{
case Direction.UP:
hero.transform.position += new Vector3(0, 0, 0.1f);
break;
case Direction.DOWN:
hero.transform.position += new Vector3(0, 0, -0.1f);
break;
case Direction.LEFT:
hero.transform.position += new Vector3(-0.1f, 0, 0);
break;
case Direction.RIGHT:
hero.transform.position += new Vector3(0.1f, 0, 0);
break;
}
}
//获取此时游戏玩家所在的方格区域
public int GetHeroArea()
{
return hero.GetComponent<HeroBehavior>().standOnArea;
}
}
3、订阅者发布者模式
发布者:
我这里的发布者是GameEventManager,用于发布游戏对象Hero逃跑得分以及Hero碰到巡逻兵游戏结束的消息。具体代码如下:
public class GameEventManager : MonoBehaviour {
public delegate void GameScoreAction();
public static event GameScoreAction gameScoreAction;
public delegate void GameOverAction();
public static event GameOverAction gameOverAction;
private SceneController sceneController;
// Use this for initialization
void Start () {
sceneController = SceneController.getInstance();
sceneController.setGameEventManager(this);
}
// Update is called once per frame
void Update () { }
//得分事件发布
public void heroEscape()
{
if (gameScoreAction != null)
gameScoreAction();
}
//游戏结束事件发布
public void patrolHitHero()
{
if (gameOverAction != null)
gameOverAction();
}
}
订阅者
这里的订阅者是GameStatusText,通过接收事件,来调节游戏中用到的text的不同。当游戏继续或者结束的时候,显示不同的信息。具体代码如下:
public class GameStatusText : MonoBehaviour {
private int score = 0;
private int textType;
//0为得分,1为游戏结束
void Start () {
distinguishText();
}
void Update () { }
void distinguishText() {
if (gameObject.name.Contains("Score"))
textType = 0;
else
textType = 1;
}
void OnEnable() {
GameEventManager.myGameScoreAction += gameScore;
GameEventManager.myGameOverAction += gameOver;
}
void OnDisable() {
GameEventManager.myGameScoreAction -= gameScore;
GameEventManager.myGameOverAction -= gameOver;
}
//得到游戏玩家得分事件
void gameScore() {
if (textType == 0) {
score++;
this.gameObject.GetComponent<Text>().text = "Score: " + score;
}
}
//得到游戏结束事件
void gameOver() {
if (textType == 1)
this.gameObject.GetComponent<Text>().text = "Game Over!";
}
}
主摄像机
在完成游戏主体代码之后,我发现当固定游戏的主摄像头时,游戏体验不好,看到的玩家比较小。所以参照其他博客,我给主摄像机加上了一个脚本,让他能随着游戏玩家运动。具体代码如下:
public class CameraBehavior : MonoBehaviour {
public GameObject target;
public float followingSpeed = 0.06f;
Vector3 distance;
// Use this for initialization
void Start () {
distance = transform.position - target.transform.position;
}
// Update is called once per frame
void Update () {
transform.position = Vector3.Lerp(transform.position, target.transform.position + distance, followingSpeed * Time.deltaTime);
}
}
游戏效果
游戏视频链接如下:https://github.com/EmilyBlues/Unity-learning/blob/master/HW6/PatrolsGame.mp4