Escape with Unity-Chan!
和 Unity 酱一起逃出生天! Unity 酱来到了一座美丽的森林,但是好像森林里并不只有她一人……WASD 控制方向 + 右键控制视角来躲避追逐而来的敌人,带着 Unity 酱逃出森林吧(并不能)!
先来看几张预览图吧!
游戏设计目标
- 创建一个地图和若干巡逻兵
- 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算
- 巡逻兵碰撞到障碍物,则会自动选下一个点为目标
- 巡逻兵在设定范围内感知到玩家,会自动追击玩家
- 失去玩家目标后,继续巡逻
- 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束
在此基础上我们做了一些小的改变:
- 第二点中,由于多数同学实现的是一个矩阵分割的地图,而我们制作的是一个开放式的森林地图,巡逻兵不适宜用固定的图形作为巡逻路径。因此,我们采用完全随机的模式来决定巡逻兵的移动路径。
- 第六点中,因为我们巡逻兵使用的是随机速度,我们想把它体现在分数上。因而每甩掉一个巡逻兵会增加相当于巡逻兵移动速度的分数。
程序设计要求
- 必须使用订阅与发布模式传消息
- Subject:OnLostGoal
- Publisher:Monster
- Subscriber:ScoreRecorder
- 工厂模式生产巡逻兵
操作方法
- WASD 控制 Unity 酱前后移动(向后移动速度较慢)和左右旋转
- 按住右键可以通过鼠标自由旋转视角
设计模式那点事
本次我们要求使用两种设计模式:发布-订阅模式和工厂模式(其实还有导演场记和单例模式),其中后者我们已经在之前的作业中领略不少,便不在此赘述了。重点来说说发布-订阅模式,这也是我觉得目前最好用的模式之一。
订阅-发布模式
如果你用过 Qt,你一定听说过信号-槽。如果你用过 JavaScript,那你一定对事件不陌生。而如果你用过 UWP,那么这个就完全对你来说不陌生了!是的,订阅-发布的本质就是提供了一种可异步的,去耦合的对象间交互模式。简单来说,发布者会发布信息(Qt 中的信号,JavaScript 中的事件),然后所有的订阅者都会收到信息并对其进行相应的处理(Qt 中的槽函数,JavaScript 中的事件处理函数)。
单是这么说可能还是有些抽象。对于咱们程序员来说,最直观的方式莫过于上代码啦~以下便是一个简单的订阅-发布模式实现:
1 | public class Publisher { |
资源库中从不缺乏惊喜
Assets Store 可以说是 Unity 小型开发中极为核心的一环,他就好比安卓的 Google Play(应用市场),或者是 IOS 的 App Store。如果把一个 Unity 程序比做一个人。那么引擎就是它的骨架,代码就是他的灵魂,而 Assets 则是它的血肉。没有他,我们的灵魂再怎么深邃,也会缺乏最直观的美感。那么来看看我们在那里面都发现了些什么吧!
Unity-Chan
作为一款免费的模型,Unity-Chan 可以说是做到了很多收费模型都做不到的程度。它有精细的贴图,恰到好处的设计,丰富的动作,以及完全开源的组件。有了它,你的游戏瞬间就会变得丰富多彩了~
Fantasy Monster
作为免费模型,Fantasy Monster也是相当良心的一款了。完备的动画、多样的造型,甚至还有攻击特效~一站式备齐你所要的一切(Monster)~
Nature Starter Kit 2
你还在为地形设计抓耳挠腮吗?你苦苦羡慕别人的光影效果吗?试试 Nature Starter Kit 2,让你的项目从此高端大气上(烧)档(显)次(卡)!体验不一样的 Unity 从此开始!
代码即是灵魂
扯了这么多,代码才是一个项目核心中的核心,下面便是这个项目中的主要代码~其中的 Animator 相关代码需要在 Unity 的预设中首先创建好 AnimationController,至于究竟如何,就交由各位自行探索啦~
GameDirector.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameDirector : System.Object
{
private static GameDirector _instance;
public SceneController currentSceneController { get; set; }
public static GameDirector getInstance()
{
if (_instance == null)
{
_instance = new GameDirector();
}
return _instance;
}
public int getFPS()
{
return Application.targetFrameRate;
}
public void setFPS(int fps)
{
Application.targetFrameRate = fps;
}
}SceneController.cs
1
2
3
4
5
6
7
8using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface SceneController
{
void LoadResources();
}FirstController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, SceneController
{
public GameObject player;
public GameObject mycamera;
public List<GameObject> monsters;
public MyFactory mF;
private bool isGameOver = false;
public bool isStart = false;
void Awake()
{
mycamera = (GameObject)Resources.Load("Prefabs/CameraContainer");
player = (GameObject)Resources.Load("Prefabs/Character");
GameDirector director = GameDirector.getInstance();
director.currentSceneController = this;
}
void Start()
{
mF = Singleton<MyFactory>.Instance;//获得工厂单例
monsters = mF.getMonsters();//从工厂获得所有的怪物
MonsterController.hitPlayerEvent += gameOver;//订阅怪物撞击玩家的事件
player = Instantiate(player);
player.transform.position = new Vector3(-53, 1.1F, 60);
mycamera = Instantiate(mycamera);
}
public bool getGameOver()
{
return isGameOver;
}
public void gameOver()
{
player.GetComponent<Animator>().SetTrigger("Lose");
this.isGameOver = true;
}
public void start()
{
Singleton<ScoreRecorder>.Instance.reset();
mF.Reput();
isStart = true;
}
public void LoadResources()
{
//
}
}MyFactory.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyFactory : MonoBehaviour {
public GameObject monster;
private List<GameObject> _monsters;
private int min_x = -80;
private int max_x = 80;
private int min_z = -80;
private int max_z = 80;
public void Awake()
{
monster = (GameObject)Resources.Load("Prefabs/Monster");
}
public List<GameObject> getMonsters()
{
List<GameObject> Monsters = new List<GameObject> ();
for (int i = 0; i < 9; ++i)
{
GameObject newMonster = Instantiate<GameObject>(monster);
newMonster.transform.position = new Vector3(500, 2, 500); // 流放
Monsters.Add(newMonster);
}
_monsters = Monsters;
return Monsters;
}
public void Reput()
{
foreach (var amonster in _monsters)
{
amonster.transform.position = new Vector3(Random.Range(min_x, max_x), 2, Random.Range(min_z, max_z));
monster.GetComponent<MonsterController>().GetNewPosition();
}
}
}MonsterController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MonsterController : MonoBehaviour {
private Vector3 NextPosition;
private int speed;
private CharacterController cc;
private Animator anim;
FirstController fc;
public delegate void hitPlayer ();
public delegate void playerLost (int score);
public static event hitPlayer hitPlayerEvent;
public static event playerLost playerLostEvent;
// Use this for initialization
void Start () {
NextPosition = transform.position;
cc = GetComponent<CharacterController>();
anim = GetComponent<Animator>();
speed = (int)Random.Range(3, 11);
fc = GameDirector.getInstance().currentSceneController as FirstController;
}
// Update is called once per frame
void FixedUpdate () {
if (fc.getGameOver())
return;
var diff = NextPosition - transform.position + Vector3.up * transform.position.y;
while (diff.magnitude < 0.1)
{
GetNewPosition();
diff = NextPosition - transform.position + Vector3.up * transform.position.y;
}
transform.LookAt(transform.position + diff);
if (diff.magnitude > speed)
cc.SimpleMove(diff / diff.magnitude * speed);
else
cc.SimpleMove(diff);
}
public void GetNewPosition()
{
NextPosition = new Vector3(transform.position.x + Random.Range(-50, 50), 0, transform.position.z + Random.Range(-50, 50));
}
void OnTriggerStay(Collider other)
{
if (fc.getGameOver())
return;
if (other.gameObject.tag == "Player")
{
NextPosition = new Vector3(other.gameObject.transform.position.x, 0, other.gameObject.transform.position.z);
}
}
void OnControllerColliderHit(ControllerColliderHit hit)
{
if (fc.getGameOver())
return;
if (hit == null)
return;
if (hit.gameObject.tag == "Player")
{
anim.SetTrigger("Attack");
hitPlayerEvent();
}
else if (hit.gameObject.tag == "Trees")
{
GetNewPosition();
}
}
private void OnTriggerExit(Collider other)
{
if (fc.getGameOver())
return;
if (other.gameObject.tag == "Player")
{
playerLostEvent(speed);
GetNewPosition();
}
}
}PlayerController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour {
CharacterController cc;
Animator anim;
FirstController fc;
private void Awake()
{
cc = GetComponent<CharacterController>();
anim = GetComponent<Animator>();
fc = GameDirector.getInstance().currentSceneController as FirstController;
}
// Use this for initialization
void Start () {
}
// Update is called once per frame
void FixedUpdate () {
if (fc.getGameOver())
return;
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
if (v > 0.1)
{
anim.SetBool("WalkingBack", false);
anim.SetBool("Run", true);
transform.Rotate(0, h * 4, 0);
cc.SimpleMove(transform.forward * 10 * v);
}
else if (v < -0.1)
{
anim.SetBool("WalkingBack", true);
anim.SetBool("Run", false);
transform.Rotate(0, h * 4, 0);
cc.SimpleMove(transform.forward * v * 3);
}
else
{
transform.Rotate(0, h * 4, 0);
anim.SetBool("Run", false);
anim.SetBool("WalkingBack", false);
}
}
}ScoreRecorder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ScoreRecorder : MonoBehaviour {
private int _score = 0;
public void Start()
{
MonsterController.playerLostEvent += Score;
}
public void Score(int score)
{
_score += score;
}
public int GetScore()
{
return _score;
}
public void reset()
{
_score = 0;
}
}UserGUI.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class UserGUI : MonoBehaviour
{
private bool isGameOver = false;
public string scoreText;
public string gameOverText;
public ScoreRecorder sR;
FirstController fc;
void Start()
{
fc = GameDirector.getInstance().currentSceneController as FirstController;
sR = Singleton<ScoreRecorder>.Instance;
scoreText = "Score: 0";
gameOverText = "Playing...";
}
void Update()
{
if (isGameOver)
{//显示结束游戏
gameOverText = "Game Over!";
return;
}
else
{
gameOverText = "Playing...";
isGameOver = fc.getGameOver();//检查游戏是否结束
scoreText = "Score: " + sR.GetScore();//显示分数
return;
}
}
void OnGUI()
{
if (fc.isStart)
{
GUI.Label(new Rect(10, 10, 100, 30), gameOverText);
GUI.Label(new Rect(10, 50, 100, 30), scoreText);
}
else
{
if (GUI.Button(new Rect(10, 10, 100, 30), "Start"))
{
fc.start();
}
}
}
}MyCameraController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32using CameraController;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MyCameraController : MonoBehaviour {
Transform Character;
public float smoothTime = 0.01f;
private Vector3 AVelocity = Vector3.zero;
Vector3 oldPosition, newPosition;
// Use this for initialization
void Start () {
Character = (GameDirector.getInstance().currentSceneController as FirstController).player.transform;
oldPosition = newPosition = Vector3.zero;
}
// Update is called once per frame
void Update () {
transform.position = Vector3.SmoothDamp(transform.position, Character.position, ref AVelocity, smoothTime);
newPosition = Input.mousePosition;
if (Input.GetMouseButton((int)MouseButtonDown.MBD_RIGHT))
{
transform.Rotate(0, (newPosition - oldPosition).x, 0);
}
else
{
transform.rotation = Character.rotation;
}
oldPosition = newPosition;
}
}
特别鸣谢
-
旭东大神的指导博客~详尽易懂,也让我知道原来 Unity 的世界可以如此精彩!
-
本作主角模型的制作团队,没有他们的努力和开源精神就不会有活力四射的 Unity 酱。
-
一款非常优秀的场景包,同时兼有相当华丽的镜头滤镜脚本,是免费玩家的不二之选。