Get Started with Unity3D - 4

Escape with Unity-Chan!

Github仓库

B站视频

和 Unity 酱一起逃出生天! Unity 酱来到了一座美丽的森林,但是好像森林里并不只有她一人……WASD 控制方向 + 右键控制视角来躲避追逐而来的敌人,带着 Unity 酱逃出森林吧(并不能)!

先来看几张预览图吧!

游戏设计目标

  1. 创建一个地图和若干巡逻兵
  2. 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算
  3. 巡逻兵碰撞到障碍物,则会自动选下一个点为目标
  4. 巡逻兵在设定范围内感知到玩家,会自动追击玩家
  5. 失去玩家目标后,继续巡逻
  6. 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束

在此基础上我们做了一些小的改变:

  1. 第二点中,由于多数同学实现的是一个矩阵分割的地图,而我们制作的是一个开放式的森林地图,巡逻兵不适宜用固定的图形作为巡逻路径。因此,我们采用完全随机的模式来决定巡逻兵的移动路径。
  2. 第六点中,因为我们巡逻兵使用的是随机速度,我们想把它体现在分数上。因而每甩掉一个巡逻兵会增加相当于巡逻兵移动速度的分数。

程序设计要求

  • 必须使用订阅与发布模式传消息
    • Subject:OnLostGoal
    • Publisher:Monster
    • Subscriber:ScoreRecorder
  • 工厂模式生产巡逻兵

操作方法

  • WASD 控制 Unity 酱前后移动(向后移动速度较慢)和左右旋转
  • 按住右键可以通过鼠标自由旋转视角

设计模式那点事

本次我们要求使用两种设计模式:发布-订阅模式和工厂模式(其实还有导演场记和单例模式),其中后者我们已经在之前的作业中领略不少,便不在此赘述了。重点来说说发布-订阅模式,这也是我觉得目前最好用的模式之一。

订阅-发布模式

如果你用过 Qt,你一定听说过信号-槽。如果你用过 JavaScript,那你一定对事件不陌生。而如果你用过 UWP,那么这个就完全对你来说不陌生了!是的,订阅-发布的本质就是提供了一种可异步的,去耦合的对象间交互模式。简单来说,发布者会发布信息(Qt 中的信号,JavaScript 中的事件),然后所有的订阅者都会收到信息并对其进行相应的处理(Qt 中的槽函数,JavaScript 中的事件处理函数)。

单是这么说可能还是有些抽象。对于咱们程序员来说,最直观的方式莫过于上代码啦~以下便是一个简单的订阅-发布模式实现:

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
public class Publisher {
public static delegate returnType Message(Type Content);
// 此处声明了委托(发布-订阅关系)的类型(或者说订阅函数的范式、签名)
// 使用此类型的委托的返回类型都是 returnType,参数都是一个 Type
public static event Message MessageSent;
// 使用 Message 类型创建一个委托,或者说一个发布-订阅关系
/*
* ...
*/
var HandledMessage = MessageSent(Some Messages);
// 此处发布了信息,内容是 Some Messages,这会通知所有订阅者对其进行处理
// 并且用 HandledMessage 来接收处理函数的返回值(可能是多个)
/*
* ...
*/
}

public class Subcriber {
Subcriber() {
Publisher.MessageSent += MessageHandler;
// 此处表示订阅 MessageSent 信息,并且用 MessageHandler 作为处理函数
}

public returnType MessageHandler(Type content) {
/*
Do something with the content
*/
}
}

资源库中从不缺乏惊喜

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
    25
    using 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
    8
    using 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
    54
    using 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
    40
    using 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
    89
    using 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
    49
    using 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
    27
    using 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
    53
    using 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
    32
    using 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;
    }
    }

特别鸣谢

  1. 陈旭东大神的 blog

    旭东大神的指导博客~详尽易懂,也让我知道原来 Unity 的世界可以如此精彩!

  2. Unity-Chan制作团队官网

    本作主角模型的制作团队,没有他们的努力和开源精神就不会有活力四射的 Unity 酱。

  3. Nature Starter Kit 2

    一款非常优秀的场景包,同时兼有相当华丽的镜头滤镜脚本,是免费玩家的不二之选。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×