3D游戏设计第七次作业——智能巡逻兵
1 任务要求
使用订阅/发布模式编写一个智能巡逻兵功能,当玩家到达巡逻兵可感知的范围中时,追逐玩家,其他时候进行巡逻。
2 作业实现
本次作业中,使用了订阅/发布模式实现场景控制器对玩家状态的订阅,巡逻兵和相应的地面作为消息的发送者,当接收到碰撞信息时,传达给事件控制器,从而使得场景控制器调用相应方法处理事件。本次作业没有使用动作分离模型,主要包含的结构是导演类、场景控制类、事件控制器、用户交互类。其中UML图如下:
本次游戏完成得较为简单,游戏效果图如下:
在题目要求的基础上没有进行过多的设定,形成的游戏玩法就是在四周有围墙,中间有十字墙的区域中,有三个巡逻兵以及他们各自管辖的地区,只要玩家进入相应的区域,就会被巡逻兵追赶,逃出一个管理区即可加一份。可玩性在于玩家与巡逻兵的速度较快且地图较小,需要玩家足够灵敏才能避免落地成盒,但熟悉之后就很简单了。不足之处还在于没有使用动作分离模型,导致代码显得有点混乱,但本来的逻辑并不难因此并不影响理解。还有就是没有实现动画控制玩家和巡逻兵的动作,仅仅只进行平移,确实偷工减料。还有并没有实现巡逻兵碰撞到墙壁后的转向,但在本地图中,每个巡逻兵仅仅管理一个正方形区域,在追赶玩家时并不会碰撞到墙壁。
3 代码解析
3.1 mvc架构
本次作业依然采用了mvc架构,实现了导演类、场景控制类、用户交互类。其中,单实例类Singleton.cs、导演类SSDirector.cs、用户交互接口IUserAction、场景控制器接口ISceneController与之前的作业差不多,第一场景控制器FirstSceneController执行了大部分游戏逻辑,内容较多。主要函数有:LoadResources加载各种资源的预制,OnEnable订阅消息,OnDisable取消订阅,SetPlayerArea设置玩家所在区域,Gameover游戏结束状态的设置。
public class FirstSceneController : MonoBehaviour, ISceneController,IUserAction
{
public GameObject plane;
public GuardFactory factory;
SSDirector director;
List<GameObject> guards;
public GameObject player;
int playerSign = 0;//0,1,2代表相应区域,3代表安全区
int gameState = 0;
int score = 0;
UserAction act;
// Start is called before the first frame update
void Awake()
{
director = SSDirector.getInstance();
director.currentSceneController = this;
gameObject.AddComponent<GuardFactory>();
gameObject.AddComponent<UserAction>();
LoadResources();
for(int i = 0;i<3;i++){
guards[i].GetComponent<Guard>().player = player;
}
player.GetComponent<Rigidbody>().freezeRotation = true;
}
// Update is called once per frame
void Update()
{
}
public int getScore(){
return score;
}
public void LoadResources(){
GameObject tmp = (GameObject)Resources.Load("Terrain");
plane = GameObject.Instantiate(tmp);
factory = Singleton<GuardFactory>.Instance;
act = Singleton<UserAction>.Instance;
player = factory.getPlayer();
player.tag = "Player";
guards = factory.getGuards();
}
public void Restart(){
gameState = 1;
for(int i = 0;i<3;i++){
guards[i].transform.position = factory.GuardPos[i];
guards[i].GetComponent<Guard>().state = 1;
}
player.transform.position = new Vector3(5,0.5f,5);
}
public void SetPlayerArea(int s){
guards[playerSign].GetComponent<Guard>().state = 1;
guards[s].GetComponent<Guard>().state = 2;
playerSign = s;
}
void OnEnable(){
GameEventManager.ScoreChange += AddScore;
GameEventManager.GameoverChange += Gameover;
}
void OnDisable(){
GameEventManager.ScoreChange -= AddScore;
GameEventManager.GameoverChange -= Gameover;
}
void AddScore(){
if(gameState!=0){
guards[playerSign].GetComponent<Guard>().state = 1;
score++;
act.setScore(score);
}
}
void Gameover(){
act.end(score);
gameState = 0;
player.transform.position = new Vector3(5,0,5);
score = 0;
act.setScore(score);
factory.Stop();
}
public int GetGameState(){
return gameState;
}
}
用户交互类UserAction主要实现的是用户进行控制游戏开始功能以及失败后得分显示的功能。
public class UserAction : MonoBehaviour
{
IUserAction iusa;
int score = 0;
bool showend = false;
int sh = 0;
// Start is called before the first frame update
void Start()
{
iusa = SSDirector.getInstance().currentSceneController as IUserAction;
}
// Update is called once per frame
void Update()
{
}
public void setScore(int s){
score = s;
}
void OnGUI(){
if(iusa.GetGameState()==0){
if(GUI.Button(new Rect(100,150,100,80),"开始")){
showend = false;
iusa.Restart();
}
if(showend){
GUI.Label(new Rect(220,220,200,80),String.Format("<color=#0000ff><size=20>游戏结束,得分:{0}</size></color>",sh));
}
}
GUI.Label(new Rect(10,10,80,80),"得分:"+score);
}
public void end(int toshow){
showend = true;
sh = toshow;
}
}
3.2 巡逻兵工厂GuardFactory
巡逻兵工厂主要产生三个巡逻兵,为每个巡逻兵添加上Guard组件。巡逻兵工厂与前一次作业中的工厂大同小异。在这里直接生产三个巡逻兵,并设定其初始位置,同时添加一个停止所有巡逻兵行为的函数,便于游戏停止时使用。在这里也顺便加载了玩家预制。
public class GuardFactory : MonoBehaviour
{
private List<GameObject> used = new List<GameObject>();
private List<GameObject> free = new List<GameObject>();
public Vector3[] GuardPos = new Vector3[3];
GameObject pre;
FirstSceneController ficon;
private bool havePro = false;
private int[] dir = {5,15,15,5};
void Awake(){
pre = (GameObject)Resources.Load("guard");
ficon = SSDirector.getInstance().currentSceneController as FirstSceneController;
}
public List<GameObject> getGuards(){
if(!havePro){
for(int i = 0;i<3;i++){
GuardPos[i] = new Vector3(dir[i],0,dir[i+1]);
}
for(int i = 0;i<3;i++){
GameObject obj = Instantiate(pre);
obj.transform.parent = ficon.plane.transform;
obj.transform.position = GuardPos[i];
obj.AddComponent<Guard>();
obj.GetComponent<Guard>().sign = i+1;
obj.GetComponent<Guard>().startPos = GuardPos[i];
used.Add(obj);
}
havePro = true;
}
return used;
}
public void freeGuard(GameObject guard){
foreach(GameObject obj in used){
if(obj.GetInstanceID() == guard.GetInstanceID()){
free.Add(guard);
used.Remove(guard);
return;
}
}
}
public GameObject getPlayer(){
return Instantiate(Resources.Load<GameObject>("gameplayer"));
}
public void Stop(){
for(int i = 0;i<3;i++){
used[i].transform.position = GuardPos[i];
used[i].GetComponent<Guard>().state = 0;
}
}
}
由于没有使用动作分离模型,巡逻兵的行动控制直接使用巡逻兵组件Guard来控制,根据当前的游戏状态及巡逻兵状态来进行行动。
public class Guard : MonoBehaviour
{
public int sign;
public float speed = 5f;
public Vector3 startPos;
int []dir = {-2,0,2,0,-2};
int nextindex = 0;
public GameObject player;
FirstSceneController fic = SSDirector.getInstance().currentSceneController as FirstSceneController;
public int state = 0;//1代表巡逻,2代表追赶玩家,0代表没开始
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if(fic.GetGameState()!=0){
Vector3 nextPos = new Vector3(startPos.x+dir[nextindex],0,startPos.z+dir[nextindex+1]);
float distance = Vector3.Distance(this.transform.position,nextPos);
if(distance<0.5){
nextindex = (nextindex+1)%4;
}
if(state==1){
transform.position = Vector3.MoveTowards(gameObject.GetComponent<Transform>().position,nextPos,speed*Time.deltaTime);
}
else if(state==2){
transform.position = Vector3.MoveTowards(gameObject.GetComponent<Transform>().position,player.transform.position,speed*Time.deltaTime);
}
}
}
}
3.3 碰撞检测
本次主要涉及的碰撞检测有:区域碰撞——玩家进入相应区域产生的碰撞,巡逻兵碰撞——玩家与巡逻兵产生的碰撞。在为玩家添加刚体与胶囊碰撞体后,为每个地面挂载一个区域碰撞脚本AreaCollide,在实现时不知道为什么检测不到碰撞开始,所以我使用了碰撞保持来设定玩家在某一区域这个状态,并使用碰撞退出来设定加分。
public class AreaCollide : MonoBehaviour
{
public int sign = 0;
FirstSceneController Ficon;
// Start is called before the first frame update
void Start()
{
Ficon = SSDirector.getInstance().currentSceneController as FirstSceneController;
}
// Update is called once per frame
void OnCollisionExit(Collision col){
if(col.gameObject.tag=="Player"){
GameEventManager.getInstance().PlayerEscape();
}
}
void OnCollisionStay(Collision col){
if(col.gameObject.tag=="Player"){
Ficon.SetPlayerArea(sign);
}
}
}
当玩家与巡逻兵碰撞,使用碰撞开始来进行事件处理。
public class PlayerCollide : MonoBehaviour
{
// Start is called before the first frame update
void OnCollisionEnter(Collision col){
if(col.gameObject.tag == "Player"){
GameEventManager.getInstance().PlayerGameover();
}
}
}
3.4 订阅/发布模式
订阅/发布模式是本次作业的重点,其中事件-代理机制就是c#对这种模式的语言实现。前面的代码中已经涉及了事件代理,场景控制器和碰撞检测器分别作为订阅者和发布者,在事件代理这个中继中传递消息与响应。场景控制器订阅了ScoreChange和GameoverChange,即为函数添加了代理,这样当事件到来时,就会通过事件管理器调用相应的函数,从而实现订阅/发布模式。
public class GameEventManager
{
public static GameEventManager Instance;
public static GameEventManager getInstance(){
if(Instance==null){
Instance = new GameEventManager();
}
return Instance;
}
public delegate void ScoreEvent();
public static event ScoreEvent ScoreChange;
public delegate void GameoverEvent();
public static event GameoverEvent GameoverChange;
private GameEventManager(){}
public void PlayerEscape(){
if(ScoreChange!=null){
ScoreChange();
}
}
public void PlayerGameover(){
if(GameoverChange!=null){
GameoverChange();
}
}
}
4 预制
本游戏涉及到的预制主要有一个地形与两个人型。人型直接采用资源商店优质免费人物,地形设置了立方体围墙和四个地面,并为他们设置相对应的标记。为了避免人物刚体碰撞产生翻转,我给玩家设置了冻结旋转,碰撞后速度为0。人物移动的脚本如下:
public class moveCube : MonoBehaviour
{
public float m_speed = 5f;
// Start is called before the first frame update
FirstSceneController fic = SSDirector.getInstance().currentSceneController as FirstSceneController;
void Start()
{
}
// Update is called once per frame
void Update()
{
if(fic.GetGameState()!=0){
if(Input.GetKey(KeyCode.RightArrow))
{
this.transform.Translate(new Vector3(m_speed * Time.deltaTime,0,0));
}
if (Input.GetKey(KeyCode.LeftArrow))
{
this.transform.Translate(new Vector3(-1 * m_speed * Time.deltaTime, 0, 0));
}
if (Input.GetKey(KeyCode.DownArrow))
{
this.transform.Translate(new Vector3(0, 0,-1 * m_speed * Time.deltaTime));
}
if (Input.GetKey(KeyCode.UpArrow))
{
this.transform.Translate(new Vector3(0, 0, m_speed * Time.deltaTime));
}
}
}
void FixedUpdate()
{
gameObject.GetComponent<Rigidbody>().velocity = Vector3.zero;
}
}