巡逻兵游戏
GitHu项目地址以及演示视频地址
作业要求
- 游戏设计要求:
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确- 定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
- 失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 程序设计要求:
- 必须使用订阅与发布模式传消息
- 工厂模式生产巡逻兵
UML图
这里主要说明一下要求中的设计模式是如何完成的:
- 工厂模式:工厂模式使用MonsterFactory类完成,这个类提供
- getInstance函数,获取工厂类实例;
- setPrefabs函数,实例化工厂类的预设;
- getMonster函数,每次被调用都返回一个战龙的object,这样firstController就可以通过调用工厂方法,生产战龙monster;
- free函数,重新开始游戏时调用,将战龙对象属性重新设置(setActive(false));
- 具体实现见项目代码。
- 发布与订阅者模式:通过ScoreRecorder类完成,这个类提供:
- addScore方法,调用一次加一分;其他类的事件通过订阅这个函数(+=)完成游戏的计分:战龙类的OnTriggerExit订阅此函数,每次玩家逃出战龙巡逻范围都能够加一分;
- setScore方法,设置分数为0,重开游戏时调用;
- getScore函数,将分数显示在屏幕上;
订阅-发布模式:
订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码
在这个游戏中,记分员类就充当发布者角色,其他涉及加分的事件对它进行订阅,这样每次加分事件发生统一由记分员完成加分,不需要各个类分别处理,从而减少耦合,达成优化目的。
最终效果
这里只是给出几个截图,具体游戏情况见视频演示(地址上面已给出)
游戏场景俯瞰图
玩家距离巡逻兵很远的时候会,巡逻兵巡逻(播放行走动画)
玩家靠近巡逻兵(进入他的感知范围),巡逻兵追捕玩家(播放奔跑动画)
玩家跳跃从而躲避巡逻兵(这个图好难截啊)
巡逻兵追到玩家,播放攻击动画,unity酱播放倒地动画
具体制作流程
预设准备:
- 在商店搜索unitychan,dragon,stonewall获取unity酱,战龙,石墙资源
动作控制设置:
unity酱的动作控制如下:
- 这里当玩家没有输入的时候,她会停在原地(idle状态);
- 当玩家按下前(↑),就会触发奔跑条件(speed>0.1),播放奔跑的动画(locomotion);
- 当玩家按下前(↓),就会触发后退条件(speed<-0.1),播放往后退的动画(walkback);
- 当玩家按下空格(space),如果是在奔跑状态下就出发跳跃条件(Bool Jump),播放跳跃动画,否则播放rest动画(rest == true,播放伸懒腰动画);
- 当unity酱被战龙追上就会出发cry条件(bool GameOver),播放unity酱哭泣动画;
战龙动作控制如下:
- 当unity酱没有进入战龙的感知范围时,战龙处于巡逻状态(Iswalk == true),播放行走动画(巡逻);
- 当unity酱进入战龙感知范围,战龙从行走状态进入奔跑状态(IsRun == true),播放奔跑动画(追击玩家);
- 当unity酱被战龙追上的时候,战龙进入攻击状态(Attack1被触发,使用setTrigger函数),播放Attack1动画,播放完一次以后回到idle状态;
地图制作:使用4个cube制作四周的墙体,然后使用6个cube制作隔离墙壁,将地图分割为四块:
- 在商店搜索unitychan,dragon,stonewall获取unity酱,战龙,石墙资源
编写代码:
unity酱控制代码:
主要是几个动作的函数,提供给firstSceneController调用:- run奔跑函数–接受用户输入控制unity酱的奔跑和转向;
- jump跳跃函数–接受用户输入控制unity酱的跳跃;
- cry哭泣–游戏结束时被调用;
using UnityEngine; using System.Collections; // 必要なコンポーネントの列記 [RequireComponent(typeof(Animator))] [RequireComponent(typeof(CapsuleCollider))] [RequireComponent(typeof(Rigidbody))] public class Player { GameObject myGirl; private bool isJump = false; public float animSpeed = 1.3f; public float jumpAnimSpeed = 1f; public float lookSmoother = 3.0f; public bool useCurves = true; public float useCurvesHeight = 1f; // 前進速度 public float forwardSpeed = 7.0f; // 後退速度 public float backwardSpeed = 5.0f; // 旋转速度 public float rotateSpeed = 1.3f; // 跳跃时施加的力 public float jumpPower = 5.0f; private CapsuleCollider col; private Rigidbody rb; private Vector3 velocity; private float orgColHight; private Vector3 orgVectColCenter; private Animator anim; private AnimatorStateInfo currentBaseState; static int idleState = Animator.StringToHash("Base Layer.Idle"); static int locoState = Animator.StringToHash("Base Layer.Locomotion"); static int jumpState = Animator.StringToHash("Base Layer.Jump"); static int restState = Animator.StringToHash("Base Layer.Rest"); public Player() { myGirl = GameObject.FindWithTag("Player"); anim = myGirl.GetComponent<Animator>(); col = myGirl.GetComponent<CapsuleCollider>(); rb = myGirl.GetComponent<Rigidbody>(); orgColHight = col.height; orgVectColCenter = col.center; } public void run(float h, float v) { anim.SetFloat("Speed", v); anim.SetFloat("Direction", h); rb.useGravity = true; velocity = new Vector3(0, 0, v); velocity = myGirl.transform.TransformDirection(velocity); if (v > 0.1) { velocity *= forwardSpeed; } else if (v < -0.1) { velocity *= backwardSpeed; } myGirl.transform.localPosition += velocity * Time.fixedDeltaTime; myGirl.transform.Rotate(0, h * rotateSpeed, 0); } public void jump() { if (isJump == false) { if (currentBaseState.fullPathHash == locoState) { if (!anim.IsInTransition(0)) { rb.AddForce(Vector3.up * jumpPower, ForceMode.VelocityChange); anim.SetBool("Jump", true); } } } } public void cry() { if (myGirl.transform.position.y > 1) { myGirl.transform.position = new Vector3(myGirl.transform.position.x, 0, myGirl.transform.position.z); } anim.SetBool("GameOver",true); } public void myUpdate() { if (myGirl.transform.position.y <= 0) { isJump = false; anim.speed = animSpeed; } else { anim.speed = jumpAnimSpeed; } currentBaseState = anim.GetCurrentAnimatorStateInfo(0); if (currentBaseState.fullPathHash == locoState) { if (useCurves) { resetCollider(); } } else if (currentBaseState.fullPathHash == jumpState) { if (!anim.IsInTransition(0)) { if (useCurves) { float jumpHeight = anim.GetFloat("JumpHeight"); float gravityControl = anim.GetFloat("GravityControl"); if (gravityControl > 0) rb.useGravity = false; Ray ray = new Ray(myGirl.transform.position + Vector3.up, -Vector3.up); RaycastHit hitInfo = new RaycastHit(); if (Physics.Raycast(ray, out hitInfo)) { if (hitInfo.distance > useCurvesHeight) { col.height = orgColHight - jumpHeight; float adjCenterY = orgVectColCenter.y + jumpHeight; col.center = new Vector3(0, adjCenterY, 0); } else { resetCollider(); } } } anim.SetBool("Jump", false); } } else if (currentBaseState.fullPathHash == idleState) { if (useCurves) { resetCollider(); } if (Input.GetButtonDown("Jump")) { anim.SetBool("Rest", true); } } else if (currentBaseState.fullPathHash == restState) { if (!anim.IsInTransition(0)) { anim.SetBool("Rest", false); } } } void resetCollider() { col.height = orgColHight; col.center = orgVectColCenter; } }
战龙控制代码:
主要是几个触发函数:- OnCollisionEnter函数–战龙发生碰撞时调用,如果撞到墙壁就转90度,撞到玩家就游戏结束;
- OnTriggerEnter函数–玩家进入战龙巡逻范围,战龙将设置isRun为true,播放奔跑动画,对玩家进行追击;
- OnTriggerStay函数–玩家停留在战龙巡逻范围的时候战龙将实时获取玩家位置,从而完成对玩家的追击(因为玩家的位置一直在变化,所以需要一直更新这个位置从而完成追击);
- OnTriggerExit–游戏结束时被调用,setTrigger(Attack_1),播放一次攻击动画,然后进入idle状态;
- FixedUpdate函数则根据战龙状态对战龙的位置进行移动(巡逻状态下没过一段时间转90度从而完成正方形巡逻、追击状态下速度加快并持续变换战龙方向使其朝向玩家,并且在朝向上发生位移达到追击效果、游戏结束则停止移动,播放一次攻击动作);
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Monster : MonoBehaviour { private Rigidbody rg; private Animator anim; private CapsuleCollider col; private AnimatorStateInfo currentBaseState; static int walkState = Animator.StringToHash("Base Layer.walk"); static int runState = Animator.StringToHash("Base Layer.run"); static int idleState = Animator.StringToHash("Base Layer.idle"); static int attackState = Animator.StringToHash("Base Layer.attack_1"); private float time; private float timePassed; private bool isRun = false; private Vector3 pos; private float length = 13; private float walkSpeed = 3; private float runSpeed = 5; // Use this for initialization void Start () { anim = GetComponent<Animator>(); col = GetComponent<CapsuleCollider>(); rg = GetComponent<Rigidbody>(); rg.useGravity = true; anim.SetBool("IsWalk", true); setTime(); } private void FixedUpdate() { if (Director.getInstance().getFirstController().isGameOver()) { return; } if (isRun == true) { anim.SetBool("IsRun",true); Vector3 dir = pos - transform.position; dir = dir.normalized; Quaternion rotation = Quaternion.LookRotation(dir, Vector3.up); rotation.x = 0; rotation.z = 0; transform.rotation = rotation; Vector3 velocity0 = new Vector3(0, 0, runSpeed); velocity0 = transform.TransformDirection(velocity0); velocity0.y = 0; transform.localPosition += velocity0 * Time.fixedDeltaTime; return; } else { anim.SetBool("IsRun", false); } if (timePassed >= time) { transform.Rotate(0, 90, 0); timePassed = 0; return; } Vector3 velocity = new Vector3(0, 0, walkSpeed); velocity = transform.TransformDirection(velocity); transform.localPosition += velocity * Time.fixedDeltaTime; timePassed += Time.fixedDeltaTime; } private void OnCollisionEnter(Collision collision) { if (collision.gameObject.tag == "Wall") { isRun = false; transform.Rotate(0, 90, 0); setTime(); } else if (collision.gameObject.tag == "Player") { Director.getInstance().getFirstController().gameOver(); anim.SetBool("IsWalk",false); anim.SetTrigger("Attack_1"); } } private void OnTriggerEnter(Collider other) { if (other.gameObject.tag == "Player") { isRun = true; pos = other.gameObject.transform.position; } } private void OnTriggerStay(Collider other) { if (other.gameObject.tag == "Player") { pos = other.gameObject.transform.position; } } private void OnTriggerExit(Collider other) { if (other.gameObject.tag == "Player") { isRun = false; ScoreRecorder.getInstance().addScore(); } } private void setTime() { System.Random random = new System.Random(); time = random.Next(2,3); timePassed = 0; } }
战龙工厂:用于生产战龙(之前UML图解释部分已经做出详细解释,这里就不说了)
using System.Collections; using System.Collections.Generic; using UnityEngine; public class MonsterFactory { private static MonsterFactory _instance; private List<GameObject> monsters; private GameObject dragon; public static MonsterFactory getInstance() { if (_instance == null) { _instance = new MonsterFactory(); } return _instance; } public void setPrefabs(GameObject prefab) { dragon = prefab; monsters = new List<GameObject>(); } public GameObject getMonster() { for (int num = 0; num < monsters.Count; num++) { if (monsters[num].activeInHierarchy == false) { monsters[num].SetActive(true); return monsters[num]; } } Debug.Log("tets"); monsters.Add(GameObject.Instantiate(dragon) as GameObject); monsters[monsters.Count - 1].SetActive(true); return monsters[monsters.Count - 1]; } // 由于不需要销毁对象,所以free不用写 public void free(GameObject temp) { temp.SetActive(false); } }
场记代码 : 提供用户接口给UserInterface类调用
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.SceneManagement; public class FirstController : MonoBehaviour, SceneController, UserAction { private bool isgameOver = false; Player myGirl; public GameObject monster; private void Awake() { Director.getInstance().setFirstController(this); MonsterFactory.getInstance().setPrefabs(monster); } // Use this for initialization void Start () { myGirl = new Player(); Director.getInstance().getFirstController().LoadResources(); } private void Update() { myGirl.myUpdate(); } public void LoadResources () { GameObject temp1 = MonsterFactory.getInstance().getMonster(); temp1.transform.position = new Vector3(3,0,0); GameObject temp2 = MonsterFactory.getInstance().getMonster(); temp2.transform.position = new Vector3(-3, 0, 0); GameObject temp3 = MonsterFactory.getInstance().getMonster(); temp3.transform.position = new Vector3(3, 0, 10); GameObject temp4 = MonsterFactory.getInstance().getMonster(); temp4.transform.position = new Vector3(-3, 0, 10); } public void movePlayer(float h, float v) { myGirl.run(h, v); } public void jump() { myGirl.jump(); } public bool isGameOver() { return isgameOver; } public void reStart() { SceneManager.LoadScene(SceneManager.GetActiveScene().name); ScoreRecorder.getInstance().setScore(0); } public void gameOver() { isgameOver = true; myGirl.cry(); } }
用户交换部分:UserInterface类–主要是接受并处理用户的前后左右输入、空格键输入、点击输入(重开游戏、游戏提示);
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class UserInterface : MonoBehaviour { private ScoreRecorder scoreRecorder; private UserAction action; // Use this for initialization void Start () { action = Director.getInstance().getFirstController(); scoreRecorder = ScoreRecorder.getInstance(); } // Update is called once per frame void Update () { if (action.isGameOver()) { action.movePlayer(0, 0); return; } if (Input.GetButtonDown("Jump")) { action.jump(); } float h = Input.GetAxis("Horizontal"); float v = Input.GetAxis("Vertical"); action.movePlayer(h, v); } private void OnGUI() { // 游戏信息 GUI.Box(new Rect(Screen.width - 260, 10, 250, 110), "操作方法"); GUI.Label(new Rect(Screen.width - 245, 30, 250, 30), "上/下 键 : 向前跑/向后退"); GUI.Label(new Rect(Screen.width - 245, 50, 250, 30), "左/右 键 : 向左转/向右转"); GUI.Label(new Rect(Screen.width - 245, 70, 250, 30), "奔跑时按下空格键 : 跳跃"); GUI.Label(new Rect(Screen.width - 245, 90, 250, 30), "停下的时候按下空格键 : 卖萌"); string gameName = "躲避怪物"; string gameRules = "操作任务躲避怪物的追捕,每躲过一只怪物分数加一"; // 显示游戏名字和游戏规则信息 if (GUI.RepeatButton(new Rect(10, 10, 120, 20), gameName)) { GUI.TextArea(new Rect(10, 40, Screen.width - 20, Screen.height / 2), gameRules); } // 重开游戏按钮 else if (GUI.Button(new Rect(140, 10, 70, 20), "重新开始")) { action.reStart(); } if (action.isGameOver()) { GUI.Box(new Rect(Screen.width - 340, 37, 70, 23), "游戏结束"); } GUI.Box(new Rect(Screen.width - 340, 10, 70, 23), "分数:" + scoreRecorder.getScore()); } }
记分员类: 这个类在前面的UML图中也说过了,就不说了
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ScoreRecorder { public static ScoreRecorder _instance; private int Score = 0; public void addScore() { Score++; } public int getScore() { return Score; } public void setScore(int num) { Score = num; } public static ScoreRecorder getInstance() { if (_instance == null) { _instance = new ScoreRecorder(); } return _instance; } }
导演类:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Director : System.Object { private static Director _instance; private FirstController firstSceneController; public static Director getInstance() { if (_instance == null) { _instance = new Director(); } return _instance; } public FirstController getFirstController() { return firstSceneController; } internal void setFirstController(FirstController gom) { if (null == firstSceneController) { firstSceneController = gom; } } }
几个接口类
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface SceneController { void LoadResources(); } using System.Collections; using System.Collections.Generic; using UnityEngine; public interface UserAction { void movePlayer(float h, float v); void jump(); bool isGameOver(); void reStart(); }
还可以与的优化
至此,游戏制作基本完成,我们还可用通过修改战龙/玩家的奔跑速度、战龙的巡逻范围、调整画面颜色等来增加游戏体验。