unity 3d 七、模型与动画
游戏要求
- 游戏规则
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家;失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
- 游戏设计要求
- 必须使用订阅与发布模式传消息
- 工厂模式生产巡逻兵
游戏改进、效果、演示视频
改进部分实现了用户视角的可切换(按"space"切换),且交互方式随视角变化,增强了游戏的交互性。
- 第三人称
![](https://gitee.com/FengFreedom/unity3-d/raw/master/homework-7/assets/around.gif)
- 第一人称
![](https://gitee.com/FengFreedom/unity3-d/raw/master/homework-7/assets/around1.gif)
完整代码
游戏UML图
![](https://gitee.com/FengFreedom/unity3-d/raw/master/homework-7/assets/uml.png)
游戏实现
PlayerController
PlayerController主要实现的是用户控制玩家,以及调用发布者在事件触发时发布消息
public class PlayerController : MonoBehaviour {
public Camera subCamera;
private Animator animator;
private AnimatorStateInfo stateInfo;
private Vector3 velocity;
private int angle;
private float rotateSpeed = 15f;
private float rotateSpeed_Sub = 60f;
private float runSpeed = 3f;
// Use this for initialization
void Start () {
animator = GetComponent<Animator> ();
angle = 1;
}
void OnTriggerEnter(Collider other) {
// 进入被监控的Area则发布信息
if (other.gameObject.CompareTag ("Area")) {
Publisher publish = Publisher.getInstance ();
int areaIndex = other.gameObject.name[other.gameObject.name.Length-1]-'0';
publish.notify (ActionType.ENTER, areaIndex, this.gameObject);
}
}
void OnCollisionEnter(Collision collision) {
// 与Patrol碰撞后发布信息
if (collision.gameObject.CompareTag ("Patrol") && animator.GetBool ("live")) {
animator.SetBool ("live", false);
Publisher publish = Publisher.getInstance ();
publish.notify (ActionType.DEAD, 0, null);
}
}
// Update is called once per frame
void FixedUpdate () {
// 当用户按下空格,改变游戏视角,通过改变子摄像头的深度实现
if (Input.GetKeyDown ("space")) {
if (angle == 1) {
subCamera.depth = -1;
angle = 3;
} else {
angle = 1;
subCamera.depth = -2;
}
}
if (!animator.GetBool ("live"))
return;
float x = Input.GetAxis ("Horizontal");
float z = Input.GetAxis ("Vertical");
if (x == 0 && z == 0) {
animator.SetBool ("Run", false);
return;
} else {
if (angle == 1) {
// 俯视视角的交互方式(WSAD分别为上下左右)
animator.SetBool ("Run", true);
velocity = new Vector3 (x, 0, z);
if (x != 0 || z != 0) {
Quaternion rotation = Quaternion.LookRotation (velocity);
if (transform.rotation != rotation)
transform.rotation = Quaternion.Slerp (transform.rotation, rotation, Time.fixedDeltaTime * rotateSpeed);
}
this.transform.position += velocity * Time.fixedDeltaTime * runSpeed;
} else {
// 第三人称视角的交互方式(WS前进、后退, AD左右旋转方向)
if (z != 0)
animator.SetBool ("Run", true);
transform.Translate(0, 0, z * runSpeed * Time.fixedDeltaTime);
transform.Rotate(0, x * rotateSpeed_Sub * Time.fixedDeltaTime, 0);
}
}
// 避免碰撞带来的影响
if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0) {
transform.localEulerAngles = new Vector3 (0, transform.localEulerAngles.y, 0);
}
if (transform.position.y != 0) {
transform.position = new Vector3 (transform.position.x, 0, transform.position.z);
}
}
}
Patrol部分
PatrolActionManager
这里主要就是提供给PatrolCrtl的动作管理器,方便了PatrolCrtl对于Patrol动作的添加、删除,而每次创建动作都会返回创建的动作作为PatrolCrtl的currentAction方便其对其进行销毁,无需再交给动作管理器销毁,那样还增加了遍历的开销。 各Action的实现十分简单,这里不多赘述,完整代码可见Github
public class PatrolActionManager : SSActionManager {
public IdleAction toIdle(GameObject obj, Animator animator, ActionCallback callback) {
IdleAction tmp = IdleAction.GetIdleAction (Random.Range (1, 2), animator);
this.addAction (obj, tmp, callback);
return tmp;
}
public WalkAction toLeft(GameObject obj, Animator animator, ActionCallback callback) {
Vector3 target = Vector3.left * Random.Range(2, 4) + obj.transform.position;
WalkAction tmp = WalkAction.GetWalkAction(target, 1.0f, animator);
this.addAction (obj, tmp, callback);
return tmp;
}
public WalkAction toRight(GameObject obj, Animator animator, ActionCallback callback) {
Vector3 target = Vector3.right * Random.Range(2, 4) + obj.transform.position;
WalkAction tmp = WalkAction.GetWalkAction(target, 1.0f, animator);
this.addAction (obj, tmp, callback);
return tmp;
}
public WalkAction toForward(GameObject obj, Animator animator, ActionCallback callback) {
Vector3 target = Vector3.forward * Random.Range(2, 4) + obj.transform.position;
WalkAction tmp = WalkAction.GetWalkAction(target, 1.0f, animator);
this.addAction (obj, tmp, callback);
return tmp;
}
public WalkAction toBack(GameObject obj, Animator animator, ActionCallback callback) {
Vector3 target = Vector3.back * Random.Range(2, 4) + obj.transform.position;
WalkAction tmp = WalkAction.GetWalkAction(target, 1.0f, animator);
this.addAction (obj, tmp, callback);
return tmp;
}
public RunAction getTarget(GameObject player, GameObject obj, Animator animator, ActionCallback callback) {
RunAction tmp = RunAction.GetRunAction (player.transform, 2.0f, animator);
this.addAction (obj, tmp, callback);
return tmp;
}
public IdleAction Stop(GameObject obj, Animator animator, ActionCallback callback) {
IdleAction tmp = IdleAction.GetIdleAction (-1f, animator);
this.addAction (obj, tmp, callback);
return tmp;
}
}
PatrolCtrl
这个类主要作用就是绑在Patrol上,控制Patrol的动作,并作为一个Observer监听事件,执行相应动作。
public class PatrolCtrl : MonoBehaviour, ActionCallback, Observer {
public enum ActionStatus: int { IDLE, TOLEFT, TOFORWARD, TORIGHT, TOBACK }
private Animator animator;
private SSAction currentAction;
private ActionStatus status;
private PatrolActionManager patrolActionManager;
// Use this for initialization
void Start () {
animator = this.gameObject.GetComponent<Animator> ();
patrolActionManager = Singleton<PatrolActionManager>.Instance;
Publisher publisher = Publisher.getInstance ();
publisher.add (this);
status = ActionStatus.IDLE;
currentAction = patrolActionManager.toIdle (gameObject, animator, this);
}
void FixedUpdate () {
if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0) {
transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
}
if (transform.position.y != 0) {
transform.position = new Vector3(transform.position.x, 0, transform.position.z);
}
gameObject.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
}
public void initial () {
status = ActionStatus.IDLE;
currentAction = patrolActionManager.toIdle (gameObject, animator, this);
}
public void removeAction () {
if (currentAction)
currentAction.destroy = true;
}
// When action done do next action
public void actionDone (SSAction source) {
status = status == ActionStatus.TOBACK ? ActionStatus.TOLEFT: (ActionStatus)((int)status + 1);
switch (status) {
case ActionStatus.TOLEFT:
currentAction = patrolActionManager.toLeft (gameObject, animator, this);
break;
case ActionStatus.TORIGHT:
currentAction = patrolActionManager.toRight (gameObject, animator, this);
break;
case ActionStatus.TOFORWARD:
currentAction = patrolActionManager.toForward (gameObject, animator, this);
break;
case ActionStatus.TOBACK:
currentAction = patrolActionManager.toBack (gameObject, animator, this);
break;
}
}
private void turn () {
currentAction.destroy = true;
switch (status) {
case ActionStatus.TOLEFT:
status = ActionStatus.TORIGHT;
currentAction = patrolActionManager.toRight (gameObject, animator, this);
break;
case ActionStatus.TORIGHT:
status = ActionStatus.TOLEFT;
currentAction = patrolActionManager.toLeft (gameObject, animator, this);;
break;
case ActionStatus.TOFORWARD:
status = ActionStatus.TOBACK;
currentAction = patrolActionManager.toBack (gameObject, animator, this);
break;
case ActionStatus.TOBACK:
status = ActionStatus.TOFORWARD;
currentAction = patrolActionManager.toForward (gameObject, animator, this);
break;
}
}
private void OnCollisionEnter (Collision collision) {
if (collision.gameObject.CompareTag ("Wall"))
turn ();
}
private void OnTriggerEnter (Collider other) {
if (other.gameObject.CompareTag ("Door"))
turn ();
}
public void notified (ActionType type, int position, GameObject actor) {
if (type == ActionType.ENTER) {
if (position == this.gameObject.name [this.gameObject.name.Length - 1] - '0') {
currentAction.destroy = true;
currentAction = patrolActionManager.getTarget (actor, gameObject, animator, this);
}
} else {
currentAction = patrolActionManager.Stop (gameObject, animator, this);
}
}
}
订阅与发布者部分
订阅者和发布者的实现方式如下,作为订阅者的类只需要继承Observer并实现其中的接口即可作为订阅者发挥作用。
订阅者和发布者这种模式的主要作用我个人觉得是方便实现组件的分离,并实现分离后组件的耦合,避免了多个功能写在一个类中,抑或是需要手动绑定多个类的情况,使得我们程序代码更加简洁,程序结构更加清晰易于理解
Observer
public interface Observer {
void notified (ActionType type, int position, GameObject actor);
}
Publisher
public enum ActionType { ENTER, EXIT, DEAD }
public class Publisher {
private delegate void ActionUpdate(ActionType type, int position, GameObject actor);
private ActionUpdate updateList;
private static Publisher _instance;
public static Publisher getInstance() {
if (_instance == null)
_instance = new Publisher();
return _instance;
}
public void notify(ActionType type, int position, GameObject actor)
{
if (updateList != null)
updateList(type, position, actor);
}
public void add(Observer observer)
{
updateList += observer.notified;
}
public void delete(Observer observer)
{
updateList -= observer.notified;
}
}
使用发布者功能的代码
void OnTriggerEnter(Collider other) {
// 进入被监控的Area则发布信息
if (other.gameObject.CompareTag ("Area")) {
Publisher publish = Publisher.getInstance ();
int areaIndex = other.gameObject.name[other.gameObject.name.Length-1]-'0';
publish.notify (ActionType.ENTER, areaIndex, this.gameObject);
}
}
void OnTriggerExit(Collider other) {
// 逃离时发布信息
if (other.gameObject.CompareTag ("Area")) {
Publisher publish = Publisher.getInstance ();
publish.notify (ActionType.EXIT, -1, this.gameObject);
}
}
void OnCollisionEnter(Collision collision) {
// 与Patrol碰撞后发布信息
if (collision.gameObject.CompareTag ("Patrol") && animator.GetBool ("live")) {
animator.SetBool ("live", false);
Publisher publish = Publisher.getInstance ();
publish.notify (ActionType.DEAD, 0, null);
}
}
ScoreRecorder
这次的计分员无须再在FirstController中控制,其作为一个观察者可以监听得分事件,从而自己改变得分
public class ScoreRecorder: Observer {
public int score = 0;
private int status = 0; // 使进出配对,避免Reset时加多一分
Text gameInfo;
private static ScoreRecorder instance;
public static ScoreRecorder getInstance()
{
if (instance == null)
{
instance = new ScoreRecorder();
}
return instance;
}
private ScoreRecorder() {
gameInfo = (GameObject.Instantiate (Resources.Load ("Prefabs/ScoreInfo")) as GameObject).transform.Find ("Text").GetComponent<Text> ();
gameInfo.text = "" + score;
status = 0;
Publisher publish = Publisher.getInstance ();
publish.add (this);
}
public void addScore() {
score += 1;
gameInfo.text = "" + score;
}
public int getScore() {
return score;
}
public void reset() {
score = 0;
status = 0;
gameInfo.text = "" + score;
}
public void notified (ActionType type, int position, GameObject actor) {
if (type == ActionType.ENTER) {
status = 1;
} else if (type == ActionType.EXIT && status == 1) {
addScore ();
}
}
}
到这里,这周的作业就基本完成了
总结
通过这次作业,我了解到了订阅者和发布者模式的使用,学会了利用订阅者和发布者模式对游戏的各个部分进行"解耦"。