一、作业要求及人工智能
这次作业的要求是做一个智能巡逻兵的小游戏。对于很多游戏来说,人工智能是不可或缺的一部分。优秀的人工智能可以让游戏更加具有挑战性,让玩家感受到更真实的游戏体验,更重要的是让游戏更加有趣。先来看看百科上关于人工智能的定义。人工智能的定义可以分为两部分,即“人工”和“智能”。“人工”比较好理解,争议性也不大。有时我们会要考虑什么是人力所能及制造的,或者人自身的智能程度有没有高到可以创造人工智能的地步,等等。但总的来说,“人工系统”就是通常意义下的人工系统。关于什么是“智能”,就问题多多了。这涉及到其它诸如意识、自我、思维(包括无意识的思维)等等问题。人唯一了解的智能是人本身的智能,这是普遍认同的观点。但是我们对我们自身智能的理解都非常有限,对构成人的智能的必要元素也了解有限,所以就很难定义什么是“人工”制造的“智能”了。因此人工智能的研究往往涉及对人的智能本身的研究。其它关于动物或其它人造系统的智能也普遍被认为是人工智能相关的研究课题。人工智能在计算机领域内,得到了愈加广泛的重视。并在机器人,经济政治决策,控制系统,仿真系统中得到应用。
游戏体验地址为http://ioogame.cn/Patrol/
二、游戏制作过程中的一些模式和框架简介
<一>MVC架构
MVC 是一种使用 MVC(Model View Controller 模型-视图-控制器)设计创建 Web 应用程序的模式:
Model(模型)表示应用程序核心(比如数据库记录列表)。
View(视图)显示数据(数据库记录)。
Controller(控制器)处理输入(写入数据库记录)。
MVC 模式同时提供了对 HTML、CSS 和 JavaScript 的完全控制。
Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。
通常模型对象负责在数据库中存取数据。
View(视图)是应用程序中处理数据显示的部分。
通常视图是依据模型数据创建的。
Controller(控制器)是应用程序中处理用户交互的部分。
通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。
MVC 分层有助于管理复杂的应用程序,因为您可以在一个时间内专门关注一个方面。例如,您可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易。
MVC 分层同时也简化了分组开发。不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。
<二>工厂模式
工厂模式是我们最常用的实例化对象模式了,是用工厂方法代替new操作的一种模式。著名的Jive论坛 ,就大量使用了工厂模式,工厂模式在Java程序系统可以说是随处可见。因为工厂模式就相当于创建实例对象的new,我们经常要根据类Class生成实例对象,如A a=new A() 工厂模式也是用来创建实例对象的,所以以后new时就要多个心眼,是否可以考虑使用工厂模式,虽然这样做,可能多做一些工作,但会给你系统带来更大的可扩展性和尽量少的修改量。
<三>订阅发布模式
发布/订阅模式定义:
又称为观察者模式,定义对象间的一种一对多的依赖关系,一个发布者可以对应多个订阅者,当发布者发生变化的时候,他可以将消息一一通知给所有的订阅者当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
发布/订阅模式解析:
UML图如下,这里发布者IPublisher提供依赖于它的订阅者的添加add和删除remove操作,同时提供一个依赖于它的所有订阅者同步的操作notify。订阅者需要提供一个update操作,当发布者发出notify通知所有订阅者时,进行调用update。
<四>门面模式
什么是门面模式
门面模式要求一个子系统的外部与其内部的通信必须通过一个统一的门面(Facade)对象进行。门面模式提供一个高层次的接口,使得子系统更易于使用。
就如同医院的接待员一样,门面模式的门面类将客户端与子系统的内部复杂性分隔开,使得客户端只需要与门面对象打交道,而不需要与子系统内部的很多对象打交道。
门面模式是对象的结构模式。门面模式没有一个一般化的类图描述,下图演示了一个门面模式的示意性对象图:
在这个对象图中,出现了两个角色:
门面(Facade)角色:客户端可以调用这个角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,本角色会将所有从客户端发来的请求委派到相应的子系统去。
子系统(subsystem)角色:可以同时有一个或者多个子系统。每一个子系统都不是一个单独的类,而是一个类的集合。每一个子系统都可以被客户端直接调用,或者被门面角色调用。子系统并不知道门面的存在,对于子系统而言,门面仅仅是另外一个客户端而已。
<五>单例模式
单例模式是设计模式中最简单的形式之一。这一模式的目的是使得类的一个对象成为系统中的唯一实例。要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,"阻止"所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。
三、游戏制作
<一>游戏资源和场景
在官网Asset Store下载Survival Shooter ,在所下载的项目中使用预制物体Environment作游戏地图就可以了。
<二>整体架构及模式关系图
<三>代码及一些要点
1.巡逻兵的路径为一个3~5个边的凸多边形,需因此要一些数组来存放巡逻兵路径的相对位置:
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)};
}
2.Subject中有一个List存放所有的Handle,有增加和删除Handle的方法Attach ()和Detach ()。还有一个Notify ()方法向每一个Handle发送消息。Handle中则有处理Subject发送过来的消息的方法Reaction ()。Player继承Subject作为发布者将自己的状态以及位置发送给每一个Handle。
public override void Notify (bool live, Vector3 pos) {
foreach (Handle h in handles) {
h.Reaction(live, pos);
}
}
3.完整代码
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;
public float smoothing = 5f;
public Text s;
public Text gg;
public Button re;
Vector3 offset;
void Start () {
action = SSDirector.getInstance ().currentScene as IUserAction;
score = SSDirector.getInstance ().currentScene as IScore;
offset = transform.position - player.position;
re.gameObject.SetActive (false);
Button btn = re.GetComponent<Button> ();
btn.onClick.AddListener(restart);
}
void Update () {
Vector3 playerCamPos = player.position + offset;
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");
}
}
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 ();
}
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 ();
}
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
}
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;
}
}
}
}
}
Handle.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface Handle {
void Reaction (bool isLive, Vector3 pos);
}
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);
}
}
}
IScor.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 () {
}
}