智能巡逻兵
要求
游戏设计要求
- 创建一个地图和若干巡逻兵(使用动画);
- 每个巡逻兵走一个 3-5 个边的凸多边型,其位置数据是相对地址,即每次确定下个目标位置时以自己当前的位置为原点计算;
- 巡逻兵碰撞到障碍物时,会自动选下一个点为目标;
- 巡逻兵能在设定范围内感知到玩家,并自动追击玩家;
- 巡逻兵失去玩家目标后,继续巡逻;
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束。
程序设计要求
- 必须使用订阅/发布模式传递消息:
subject:OnLostGoal
Publisher: ?
Subscriber: ?
- 使用工厂模式来生产巡逻兵。
提示
- 生成 3-5个边的凸多边型:
随机生成矩形,在矩形每个边上随机找点,可得到 3-4 的凸多边型;在矩形每个边上随机找点且在其中一条边上找2个不同的点,可得到 4-5 的凸多边型;
- 可以参考以前博客,给出新玩法。
相关理论
模型
- 物体对象的组合,Unity 映射为游戏对象树;
- 每个物体包含一个网格(Mesh)与蒙皮设置;
- 包含使用的纹理、材质以及一个或多个动画剪辑;
- 模型由 3D 设计人员使用 3D Max 等创建;
- 显示:Mesh 表面网格,MeshRender 网格渲染器。
动画剪辑
- 物体或骨骼的位置等属性在关键帧的时间序列;
- 通过插值算法计算每帧的物体的位置或属性;
- 可能包括:在动作捕捉工作室中捕捉的人形动画,美术师在外部 3D 应用程序中从头开始创建的动画,来自第三方库的动画集,从导入的单个时间轴剪切的多个剪辑。
三维模型是用三维建模软件建造的立体模型,也是构成 Unity 3D 场景的基础元素。
- Unity 3D 几乎支持所有主流格式的三维模型,如 FBX 文件和 OBJ 文件等;
- 开发者可以将三维建模软件导出的模型文件添加到项目资源文件夹,其会显示在 Assets 面板中;
- 主流的三维建模软件:Autodesk 3D Studio Max,Autodesk Maya,Cinema 4D。
Mecanim 动画系统
一个丰富而复杂的动画系统(Mecanim),具有以下功能:
- 为 Unity 的所有元素(包括对象、角色和属性)提供简单工作流程和动画设置;
- 支持导入的动画剪辑以及 Unity 内创建的动画;
- 人形动画重定向 - 能够将动画从一个角色模型应用到另一角色模型;
- 对齐动画剪辑的简化工作流程;
- 方便预览动画剪辑以及它们之间的过渡和交互;
- 提供可视化编程工具来管理动画之间的复杂交互;
- 以不同逻辑对不同身体部位进行动画化;
- 分层和遮罩功能。
Animator 组件用于将动画分配给场景中的游戏对象,需要引用 Animator Controller,后者定义要使用哪些动画剪辑,并控制何时以及如何在动画剪辑之间进行混合和过渡。有动画的模型,编辑器会自动添加该组件;用户也可以在模型根对象手动添加该组件。
动画控制器:Mecanim 使用状态机管理运动。
设计状态机控制器: 状态、变迁、参数、条件四个部分设计。
动画控制器基础编程:设计时,功能强大;运行时,甚至无法查询设计了哪些状态。
动画控制器事件:行为与事件,API脚本。
实现过程与代码
面向对象的编程 - 设计模式:对象的行为
行为型模式分为类行为模式(采用继承机制来在类间分派行为)和对象行为模式(采用组合或聚合在对象间分配行为),用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,涉及算法与对象间职责的分配,其中对象行为模式比类行为模式具有更大的灵活性。
行为型模式是 GoF 设计模式中最为庞大的一类,包含模板方法模式、策略模式、命令模式、职责链模式、状态模式、观察者模式、中介者模式、迭代器模式、访问者模式、备忘录模式、解释器模式11种模式。
依赖倒置(DIP)降低了客户与实现模块之间的耦合:
- 原始代码 Sender 依赖 Receiver,Receiver 不依赖 sender;
- 添加 IXxxListener 接口后, Receiver 依赖 IXxxListener 和 Sender;Sender 依赖 IXxxListener。
MVC 架构工厂模式
订阅/发布模式
UML图:
-
发布者(事件源)/Publisher:事件或消息的拥有者;
-
主题(渠道)/Subject:消息发布媒体;
-
接收器(地址)/Handle:处理消息的方法;
-
订阅者(接收者)/Subscriber:对主题感兴趣的人。
这也被称为观察者模式:
- 发布者与订阅者没有直接的耦合;
- 发布者与订阅者没有直接关联,可以多对多通讯;
- 在MVC设计中,是实现模型与视图分离的重要手段(例如数据 DataSource 对象,就是 Subject,任何使用该数据源的显示控件,如Grid都会及时更新)。
设计模式与游戏的持续改进
- 当一个游戏对象实现“击打”行为,可能的处理是:
按规则计分;
按规则计算对周边物体的伤害;
显示各种效果;
… …
- 如果在事件写了以上代码:
想修改游戏规则,将无法保证修改正确,因为很多行为都有类似的代码;
添加一些新行为,不仅无法复用代码,而且产生逻辑冲突。
- 如果使用设计模式:
计分员对象感兴趣该事件,会计分;
控制器感兴趣该事件,会按规则做响应;
添加新需求,如生成奖励对象,则添加一个奖励管理者。
代码
SSDirector.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object {
//singleton instance
private static SSDirector instance;
public ISceneController currentScene;
public bool running {
get;
set;
}
public static SSDirector getInstance () {
if (instance == null) {
instance = new SSDirector ();
}
return instance;
}
}
ISceneController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController {
void LoadResources ();
void CreatePatrols ();
void CreateMore ();
}
SceneController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using com.myspace;
public class SceneController : MonoBehaviour, ISceneController, IUserAction, IScore, Handle {
public GameObject player;
private SSDirector director;
private bool canOperation;
private bool create;
private int bearNum;
private int ellephantNum;
private Subject sub;
private Animator ani;
private Vector3 movement; // The vector to store the direction of the player's movement.
void Awake () {
director = SSDirector.getInstance ();
sub = player.GetComponent<Player> ();
ani = player.GetComponent<Animator> ();
director.currentScene = this;
director.currentScene.LoadResources ();
director.currentScene.CreatePatrols ();
Handle sc = director.currentScene as Handle;
sub.Attach (sc);
GetComponent<ScoreManager> ().resetScore ();
bearNum = 0;
ellephantNum = 0;
create = false;
}
void Update () {
int score = GetComponent<ScoreManager> ().getScore ();
if (score % 10 == 0) {
director.currentScene.CreateMore ();
} else {
create = true;
}
}
#region ISceneController
public void LoadResources () {
GameObject Environment = Instantiate<GameObject> (
Resources.Load<GameObject> ("Prefabs/Environment"));
Environment.name = "Environment";
}
public void CreatePatrols () { //创建游戏开始时的巡逻兵
PatrolFactory pf = PatrolFactory.getInstance ();
for (int i = 1; i <= 12; i++) {
GameObject patrol = pf.getPatrol ();
patrol.name = "Patrol" + ++bearNum;
Handle p = patrol.GetComponent<Patrol> ();
sub.Attach (p);
patrol.GetComponent<Patrol> ().register (GetComponent<ScoreManager> ().addScore);
}
}
public void CreateMore () { //每增加十分,创建新的巡逻兵
if (create) {
PatrolFactory pf = PatrolFactory.getInstance ();
for (int i = 1; i <= 3; i++) {
GameObject patrol = pf.getPatrol ();
patrol.name = "Patrol" + ++ellephantNum;
Handle p = patrol.GetComponent<Patrol> ();
sub.Attach (p);
patrol.GetComponent<Patrol> ().register (GetComponent<ScoreManager> ().addScore);
}
for (int i = 1; i <= 3; i++) {
GameObject patrolplus = pf.getPatrolPlus ();
patrolplus.name = "Patrolplus" + ++bearNum;
Handle p = patrolplus.GetComponent<Patrol> ();
sub.Attach (p);
patrolplus.GetComponent<Patrol> ().register (GetComponent<ScoreManager> ().addScore);
}
create = false;
}
}
#endregion
#region IUserAction
public void movePlayer (float h, float v) {
if (canOperation) {
player.GetComponent<Player> ().move (h, v);
if (h == 0 && v == 0) {
ani.SetTrigger ("stop");
} else {
ani.SetTrigger ("move");
}
}
}
public void setDirection (float h, float v) {
if (canOperation) {
player.GetComponent<Player> ().turn (h, v);
}
}
public bool GameOver () {
return (!canOperation);
}
#endregion
#region ISceneController
public int currentScore () {
return GetComponent<ScoreManager> ().getScore ();
}
#endregion
#region Handele
public void Reaction (bool isLive, Vector3 pos) {
ani.SetBool ("live", isLive);
canOperation = isLive;
}
#endregion
}
IUserAction.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IUserAction {
void movePlayer (float h, float v);
void setDirection (float h, float v);
bool GameOver ();
}
UI.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class UI : MonoBehaviour {
private IUserAction action;
private IScore score;
public Transform player; // The position that that camera will be following.
public float smoothing = 5f; // The speed with which the camera will be following.
public Text s;
public Text gg;
public Button re;
Vector3 offset; // The initial offset from the player.
// Use this for initialization
void Start () {
action = SSDirector.getInstance ().currentScene as IUserAction;
score = SSDirector.getInstance ().currentScene as IScore;
// Calculate the initial offset.
offset = transform.position - player.position;
re.gameObject.SetActive (false);
Button btn = re.GetComponent<Button> ();
btn.onClick.AddListener(restart);
}
void Update () {
// Create a postion the camera is aiming for based on the offset from the player.
Vector3 playerCamPos = player.position + offset;
// Smoothly interpolate between the camera's current position and it's player position.
transform.position = Vector3.Lerp (transform.position, playerCamPos, smoothing * Time.deltaTime);
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
move (h, v);
turn (h, v);
showScore ();
gameOver ();
}
//移动玩家
public void move (float h, float v) {
action.movePlayer (h, v);
}
//使玩家面向移动方向
public void turn (float h, float v) {
if (h != 0 || v != 0) {
action.setDirection (h, v);
}
}
//显示分数
public void showScore () {
s.text = "Score : " + score.currentScore ();
}
//游戏结束
public void gameOver () {
if (action.GameOver ()) {
if (!re.isActiveAndEnabled) {
re.gameObject.SetActive (true);
}
gg.text = "Game Over!";
}
}
//重新开始
public void restart () {
SceneManager.LoadScene ("main");
}
}
Subject.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public abstract class Subject : MonoBehaviour {
protected List<Handle> listeners = new List<Handle> ();
public abstract void Attach (Handle listener);
public abstract void Detach (Handle listener);
public abstract void Notify (bool live, Vector3 pos);
}
Handle.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface Handle {
void Reaction (bool isLive, Vector3 pos);
}
Player.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : Subject {
private bool isLive;
private Vector3 position;
private float speed;
Vector3 movement; // The vector to store the direction of the player's movement.
protected List<Handle> handles = new List<Handle> (); //所有观察者
// Use this for initialization
void Start () {
isLive = true;
speed = 8.0f;
}
public override void Attach (Handle h) {
handles.Add (h);
}
public override void Detach (Handle h) {
handles.Remove (h);
}
public override void Notify (bool live, Vector3 pos) {
foreach (Handle h in handles) {
h.Reaction(live, pos);
}
}
//玩家碰到巡逻兵,就死亡
void OnCollisionEnter (Collision other) {
if (other.gameObject.tag == "patrol") {
isLive = false;
}
}
// Update is called once per frame
void Update () {
position = transform.position;
Notify (isLive, position);
}
public void move (float h, float v) {
if (isLive) {
// Set the movement vector based on the axis input.
movement.Set (h, 0f, v);
// Normalise the movement vector and make it proportional to the speed per second.
movement = movement.normalized * speed * Time.deltaTime;
// Move the player to it's current position plus the movement.
GetComponent<Rigidbody> ().MovePosition (transform.position + movement);
}
}
public void turn (float h, float v) {
if (isLive) {
// Set the movement vector based on the axis input.
movement.Set (h, 0f, v);
Quaternion rot = Quaternion.LookRotation (movement);
// Set the player's rotation to this new rotation.
GetComponent<Rigidbody> ().rotation = rot;
}
}
}
Patrol.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Patrol : MonoBehaviour, Handle {
protected Vector3 bothPos;
private bool playerIsLive;
private Vector3 playerPos;
private Vector3[] posSet;
private int currentSide;
private int sideNum;
private bool turn;
private bool isCatching = false;
public int score = 1;
public float field = 7f;
public float speed = 1f;
public delegate void getScore (int n);
public event getScore escape;
public void register (getScore s) {
escape += s;
}
public void unRegister (getScore s) {
escape -= s;
}
// Use this for initialization
void Start () {
transform.position = getBothPos ();
bothPos = transform.position;
}
void Awake () {
turn = false;
sideNum = Random.Range (3, 6);
currentSide = 0;
if (sideNum == 3) {
posSet = new Vector3[] { new Vector3 (0, 0, 0), new Vector3 (8, 0, 0),
new Vector3 (4, 0, 6), new Vector3 (0, 0, 0) };
} else if (sideNum == 4) {
posSet = new Vector3[] { new Vector3 (0, 0, 0), new Vector3 (8, 0, 0),
new Vector3 (8, 0, 8), new Vector3 (0, 0, 8), new Vector3 (0, 0, 0) };
} else {
posSet = new Vector3[] { new Vector3 (0, 0, 0), new Vector3 (5, 0, 0),
new Vector3 (7, 0, 5), new Vector3 (3, 0, 8), new Vector3 (-2, 0, 5), new Vector3 (0, 0, 0) };
}
}
void OnCollisionEnter (Collision other) {
turn = true;
}
public bool inField (Vector3 targetPos) {
float distance = (transform.position - targetPos).sqrMagnitude;
if (distance <= field * field) {
return true;
}
return false;
}
public void Reaction (bool isLive, Vector3 pos) {
playerIsLive = isLive;
playerPos = pos;
}
public void catchPlayer () {
bothPos = transform.position;
isCatching = true;
transform.LookAt (playerPos);
transform.position = Vector3.Lerp (transform.position, playerPos, speed * Time.deltaTime);
}
public bool patrolInMap (int side) {
if (isCatching && playerIsLive) {
isCatching = false;
if (escape != null) {
escape (score);
}
}
if (turn) {
turn = false;
Vector3 v = transform.forward;
Quaternion dir = Quaternion.LookRotation (v);
Quaternion toDir = Quaternion.LookRotation (-v);
transform.rotation = Quaternion.RotateTowards (dir, toDir, 1f);
return true;
}
if (transform.position != bothPos + posSet [side + 1]) {
transform.LookAt (bothPos + posSet [side + 1]);
transform.position = Vector3.Lerp (transform.position ,
bothPos + posSet [side + 1], speed * Time.deltaTime);
}
if ((transform.position - (bothPos + posSet [side + 1])).sqrMagnitude <= 0.1f) {
return true;
}
return false;
}
public Vector3 getBothPos () {
while (true) {
Vector3 pos = new Vector3 (Random.Range (-30f, 30f), 0, Random.Range (-30f, 30f));
if ((pos - Vector3.zero).sqrMagnitude >= 100f) {
return pos;
}
}
}
// Update is called once per frame
void Update () {
if (playerIsLive && inField (playerPos)) {
catchPlayer ();
} else {
if (patrolInMap (currentSide)) {
if (++currentSide >= sideNum) {
bothPos = transform.position;
currentSide = 0;
}
}
}
}
}
PatrolFactory.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace com.myspace{
public class PatrolFactory : System.Object {
private static PatrolFactory instance;
public static PatrolFactory getInstance () {
if (instance == null) {
instance = new PatrolFactory ();
}
return instance;
}
public GameObject getPatrol () {
GameObject patrol = GameObject.Instantiate<GameObject> (
Resources.Load<GameObject> ("Prefabs/Patrol"));;
return patrol;
}
public GameObject getPatrolPlus () {
GameObject patrolplus = GameObject.Instantiate<GameObject> (
Resources.Load<GameObject> ("Prefabs/Patrolplus"));;
return patrolplus;
}
public void freePatrol (GameObject p) {
p.SetActive (false);
}
}
}
IScore.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IScore {
int currentScore ();
}
ScoreManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreManager : MonoBehaviour, Handle {
private int score;
private bool playerIsLive;
public void Reaction (bool isLive, Vector3 pos) {
playerIsLive = isLive;
}
public int getScore () {
return score;
}
public void addScore (int s) {
if (playerIsLive) {
score += s;
}
}
public void resetScore () {
score = 0;
}
void Awake () {
playerIsLive = true;
score = 0;
}
void Update () {}
}