3D Computer Game Programming-Note 4

1、基本操作演练

  • 下载 Fantasy Skybox FREE,构建自己的游戏场景

    • Asset Store 上下载 Fantasy Skybox FREE:
      Unity-游戏对象与图形基础 - 图1

    • 导入包到 Assets 中:
      Unity-游戏对象与图形基础 - 图2

    • 在 Main Camera 上添加部件 Rendering -> Skybox,在已导入的 Fantasy Skybox FREE -> Materials 中选择喜欢的素材(.mat 文件)拖放到 Skybox 中:
      Unity-游戏对象与图形基础 - 图3

运行:
Unity-游戏对象与图形基础 - 图4

  • 在菜单栏选择 GameObject -> 3D Object -> Terrain,新建一个地形对象,同时为了更完整的视觉效果,在其附近创建另外三个地形对象:
    Unity-游戏对象与图形基础 - 图5

Unity-游戏对象与图形基础 - 图6

  • 在 Terrain 的 Inspector 窗口中选择合适的工具渲染起伏的地形、花草等,从 Asset Store 中导入水面预制,游戏场景最终效果如下:
    Unity-游戏对象与图形基础 - 图7
  • 写一个简单的总结,总结游戏对象的使用。

  作为 Unity 中的基本对象,游戏对象可以作为组件的容器使用,即通过在游戏对象上挂载不同的组件来获得相关的属性,从而实现不同的功能。

  比如,当希望得到一个视觉效果惊艳的游戏对象时,可以挂载 Material 调整颜色透明度等,或是挂载 Texture2D 为其贴上纹理;当希望一个游戏对象可以作为一个音频播放器时,可以在该游戏对象上挂载一个 Audio;当希望游戏对象可以在游戏运行时自动完成某些动作时,可以编写脚本并挂载到游戏对象上,在运行时自动执行脚本指定的逻辑实现。

  游戏对象既可以通过直接创建的方式实例化,也可以通过预制来实例化。

2、实现《牧师与魔鬼》动作分离版

  1. 要求:设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束

  本次游戏是在🔗牧师与恶魔的基础上将动作管理与游戏场景分离而实现的。借用课程主页的设计图:

Unity-游戏对象与图形基础 - 图8

  设计思路如下:

  • 通过门面模式(控制器模式)输出组合好的几个动作,共原来程序调用
    • 这个门面就是 CCActionManager
  • 通过组合模式实现动作组合,按组合模式设计方法
    • 必须有一个抽象事物表示该类事物的共性,例如 SSAction,表示动作,不管是基本动作或是组合后的动作
    • 基本动作,用户设计的基本动作类。 例如:CCMoveToAction
    • 组合动作,由(基本或组合)动作组合的类。例如:CCSequenceAction
  • 接口回调(函数回调)实现管理者与被管理者解耦
    • 如组合对象实现一个事件抽象接口(ISSCallback),作为监听器(listener)监听子动作的事件
    • 被组合对象使用监听器传递消息给管理者。至于管理者如何处理由实现该监听器的人决定
  • 通过模板方法,让使用者减少对动作管理过程细节的要求
    • SSActionManager 作为 CCActionManager 基类

  根据该设计思路,修改程序结构,对应的 UML 图如下:

Unity-游戏对象与图形基础 - 图9

  为了实现动作分离,需要新增以下文件:

  • SSAction.cs: 继承 ScriptableObject(不需要绑定 GameObject 对象的可编程基类),作为游戏动作的基类

    1. public class SSAction : ScriptableObject {
    2. public bool enable = true;
    3. public bool destory = false;
    4. public GameObject gameObject { get; set; }
    5. public Transform transform { get; set; }
    6. public ISSActionCallback callback { get; set; }
    7. protected SSAction() { }
    8. public virtual void Start() {
    9. throw new System.NotImplementedException();
    10. }
    11. public virtual void Update() {
    12. throw new System.NotImplementedException();
    13. }
    14. }
  • CCMoveToAction.cs: 实现具体动作,将一个物体移动到目标位置,并通知任务完成

    1. public class CCMoveToAction : SSAction {
    2. public Vector3 target;
    3. public float speed;
    4. public static CCMoveToAction GetSSAction(Vector3 target, float speed) {
    5. CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
    6. action.target = target;
    7. action.speed = speed;
    8. return action;
    9. }
    10. private CCMoveToAction() { }
    11. public override void Start() { }
    12. public override void Update() {
    13. if (this.gameObject == null || this.transform.localPosition == target)
    14. {
    15. this.destory = true;
    16. this.callback.SSActionEvent(this);
    17. return;
    18. }
    19. this.transform.localPosition = Vector3.MoveTowards(this.transform.localPosition, target, speed * Time.deltaTime);
    20. }
    21. }
  • CCSequenceAction.cs: 实现一个动作组合序列,顺序播放动作。在本游戏中主要服务于角色上下船的运动轨迹(只有一个移动动作时角色会沿直线而非折线运动,出现穿过其他游戏对象的情况)

    1. public class CCSequenceAction : SSAction, ISSActionCallback {
    2. public List<SSAction> sequence;
    3. public int repeat = -1;
    4. public int start = 0;
    5. public static CCSequenceAction GetSSAction(int repeat, int start, List<SSAction> sequence) {
    6. CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
    7. action.repeat = repeat;
    8. action.sequence = sequence;
    9. action.start = start;
    10. return action;
    11. }
    12. public override void Start() {
    13. foreach (SSAction action in sequence) {
    14. action.gameObject = this.gameObject;
    15. action.transform = this.transform;
    16. action.callback = this;
    17. action.Start();
    18. }
    19. }
    20. public override void Update() {
    21. if (sequence.Count == 0)
    22. return;
    23. if (start < sequence.Count)
    24. sequence[start].Update();
    25. }
    26. public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competed, int Param = 0, string strParam = null, Object objectParam = null) {
    27. source.destory = false;
    28. this.start++;
    29. if (this.start >= sequence.Count) {
    30. this.start = 0;
    31. if (repeat > 0)
    32. repeat--;
    33. if (repeat == 0) {
    34. this.destory = true;
    35. this.callback.SSActionEvent(this);
    36. }
    37. }
    38. }
    39. void OnDestory() { }
    40. }
  • SSActionManager.cs: 作为动作对象管理器的基类,实现了所有动作的基本管理

    1. public class SSActionManager : MonoBehaviour {
    2. private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    3. private List<SSAction> waitingAdd = new List<SSAction>();
    4. private List<int> waitingDelete = new List<int>();
    5. protected void Start() { }
    6. protected void Update() {
    7. foreach (SSAction ac in waitingAdd)
    8. actions[ac.GetInstanceID()] = ac;
    9. waitingAdd.Clear();
    10. foreach (KeyValuePair<int, SSAction> kv in actions) {
    11. SSAction ac = kv.Value;
    12. if (ac.destory) {
    13. waitingDelete.Add(ac.GetInstanceID());
    14. }
    15. else if (ac.enable) {
    16. ac.Update();
    17. }
    18. }
    19. foreach (int key in waitingDelete) {
    20. SSAction ac = actions[key];
    21. actions.Remove(key);
    22. Destroy(ac);
    23. }
    24. waitingDelete.Clear();
    25. }
    26. public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager) {
    27. action.gameObject = gameObject;
    28. action.transform = gameObject.transform;
    29. action.callback = manager;
    30. waitingAdd.Add(action);
    31. action.Start();
    32. }
    33. }
  • CCActionManager.cs: 封装游戏中的具体动作,提供接口供场景控制器调用,实现动作管理与游戏场景分离

    1. public class CCActionManager : SSActionManager, ISSActionCallback {
    2. private bool isMoving = false;
    3. public CCMoveToAction MoveBoatAction;
    4. public CCSequenceAction MoveRoleAction;
    5. public FirstController sceneController;
    6. public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competed, int Param = 0, string strParam = null, Object objectParam = null) {
    7. isMoving = false;
    8. }
    9. protected new void Start() {
    10. sceneController = (FirstController)SSDirector.GetInstance().CurrentSceneController;
    11. sceneController.SetActionManager(this);
    12. }
    13. public bool GetIsMoving() {
    14. return isMoving;
    15. }
    16. public void MoveBoat(GameObject obj, Vector3 target, float speed) {
    17. if (isMoving) {
    18. return;
    19. }
    20. isMoving = true;
    21. MoveBoatAction = CCMoveToAction.GetSSAction(target, speed);
    22. this.RunAction(obj, MoveBoatAction, this);
    23. }
    24. public void MoveRole(GameObject role, Vector3 transfer, Vector3 target, float speed) {
    25. if (isMoving) {
    26. return;
    27. }
    28. isMoving = true;
    29. MoveRoleAction = CCSequenceAction.GetSSAction(0, 0, new List<SSAction> { CCMoveToAction.GetSSAction(transfer, speed), CCMoveToAction.GetSSAction(target, speed) });
    30. this.RunAction(role, MoveRoleAction, this);
    31. }
    32. }
  • ISSActionCallback.cs: 实现消息通知,避免与动作管理者直接依赖

    1. public enum SSActionEventType : int { Started, Competed }
    2. public interface ISSActionCallback {
    3. void SSActionEvent(SSAction source,
    4. SSActionEventType events = SSActionEventType.Competed,
    5. int intParam = 0,
    6. string strParam = null,
    7. Object objectParam = null);
    8. }
  • Judge.cs: 裁判类,当游戏达到结束条件时,通知场景控制器游戏结束

    1. public class Judge : MonoBehaviour {
    2. public FirstController sceneController;
    3. public CoastModel srcCoastModel;
    4. public CoastModel desCoastModel;
    5. public BoatModel boatModel;
    6. void Start() {
    7. sceneController = (FirstController)SSDirector.GetInstance().CurrentSceneController;
    8. srcCoastModel = sceneController.SrcCoastController.GetCoastModel();
    9. desCoastModel = sceneController.DesCoastController.GetCoastModel();
    10. boatModel = sceneController.boatController.GetBoatModel();
    11. }
    12. // 参考之前的 Check()
    13. void Update() {
    14. if (!sceneController.isRunning)
    15. return;
    16. if (sceneController.time <= 0) {
    17. sceneController.JudgeCallback("Game Over!", false);
    18. return;
    19. }
    20. this.gameObject.GetComponent<UserGUI>().result = "";
    21. if (desCoastModel.priestNum == 3) {
    22. sceneController.JudgeCallback("You Win!", false);
    23. return;
    24. }
    25. else {
    26. int leftPriestNum, leftDevilNum, rightPriestNum, rightDevilNum;
    27. leftPriestNum = srcCoastModel.priestNum + (boatModel.OnRight ? 0 : boatModel.priestNum);
    28. leftDevilNum = srcCoastModel.devilNum + (boatModel.OnRight ? 0 : boatModel.devilNum);
    29. if (leftPriestNum != 0 && leftPriestNum < leftDevilNum) {
    30. sceneController.JudgeCallback("Game Over!", false);
    31. return;
    32. }
    33. rightPriestNum = desCoastModel.priestNum + (boatModel.OnRight ? boatModel.priestNum : 0);
    34. rightDevilNum = desCoastModel.devilNum + (boatModel.OnRight ? boatModel.devilNum : 0);
    35. if (rightPriestNum != 0 && rightPriestNum < rightDevilNum) {
    36. sceneController.JudgeCallback("Game Over!", false);
    37. return;
    38. }
    39. }
    40. }
    41. }

  同时,需要更改以下文件:

  • FirstController.cs: 主要更改与动作管理相关的部分(与上一版相同的部分代码略去)

    1. public class FirstController : MonoBehaviour, ISceneController, IUserAction {
    2. /* 增加动作管理器 */
    3. public CCActionManager ActionManager;
    4. public CoastController DesCoastController;
    5. public CoastController SrcCoastController;
    6. public BoatController boatController;
    7. public RoleController[] roleModelControllers;
    8. //private MoveController moveController;
    9. private RiverModel river;
    10. public bool isRunning;
    11. public float time;
    12. public float speed = 8;
    13. public void SetActionManager(CCActionManager actionManager) {
    14. this.ActionManager = actionManager;
    15. }
    16. void Awake() {
    17. SSDirector director = SSDirector.GetInstance();
    18. director.CurrentSceneController = this;
    19. director.CurrentSceneController.LoadResources();
    20. this.gameObject.AddComponent<UserGUI>();
    21. this.gameObject.AddComponent<CCActionManager>();
    22. this.gameObject.AddComponent<Judge>();
    23. }
    24. public void LoadResources() {
    25. /*...*/
    26. }
    27. /* 使用动作管理器提供的接口实现运动,取代原来的 moveController;下同 */
    28. public void MoveBoat() {
    29. if ((!isRunning) || ActionManager.GetIsMoving())
    30. return;
    31. if (boatController.GetBoatModel().OnRight)
    32. ActionManager.MoveBoat(boatController.GetBoatModel().boat, PositionModel.boat_on_left, speed);
    33. else
    34. ActionManager.MoveBoat(boatController.GetBoatModel().boat, PositionModel.boat_on_right, speed);
    35. boatController.GetBoatModel().OnRight = !boatController.GetBoatModel().OnRight;
    36. }
    37. public void MoveRole(RoleModel roleModel) {
    38. if ((!isRunning) || ActionManager.GetIsMoving())
    39. return;
    40. Vector3 target, transfer;
    41. if (roleModel.OnBoat) {
    42. if (boatController.GetBoatModel().OnRight)
    43. target = DesCoastController.AddRole(roleModel);
    44. else
    45. target = SrcCoastController.AddRole(roleModel);
    46. /* 设置一个中转点使运动轨迹成为折线;下同 */
    47. if (roleModel.role.transform.localPosition.y > target.y)
    48. transfer = new Vector3(target.x, roleModel.role.transform.localPosition.y, target.z);
    49. else
    50. transfer = new Vector3(roleModel.role.transform.localPosition.x, target.y, target.z);
    51. ActionManager.MoveRole(roleModel.role, transfer, target, 5);
    52. roleModel.OnRight = boatController.GetBoatModel().OnRight;
    53. boatController.RemoveRole(roleModel);
    54. }
    55. else {
    56. if (boatController.GetBoatModel().OnRight == roleModel.OnRight) {
    57. if (roleModel.OnRight) {
    58. DesCoastController.RemoveRole(roleModel);
    59. }
    60. else {
    61. SrcCoastController.RemoveRole(roleModel);
    62. }
    63. target = boatController.AddRole(roleModel);
    64. if (roleModel.role.transform.localPosition.y > target.y)
    65. transfer = new Vector3(target.x, roleModel.role.transform.localPosition.y, target.z);
    66. else
    67. transfer = new Vector3(roleModel.role.transform.localPosition.x, target.y, target.z);
    68. ActionManager.MoveRole(roleModel.role, transfer, target, 5);
    69. }
    70. }
    71. }
    72. public void Restart() {
    73. /*...*/
    74. }
    75. void Update() {
    76. if (isRunning) {
    77. time -= Time.deltaTime;
    78. this.gameObject.GetComponent<UserGUI>().time = (int)time;
    79. }
    80. }
    81. /* 将裁判类的返回信息呈现在游戏场景中 */
    82. public void JudgeCallback(string result, bool isRunning) {
    83. this.gameObject.GetComponent<UserGUI>().result = result;
    84. this.gameObject.GetComponent<UserGUI>().time = (int)time;
    85. this.isRunning = isRunning;
    86. }
    87. }

  此外,由于增加了游戏场景,上一版中由长方体构成的“粗制滥造”的 River 已经不需要了,因此将所有文件中有关 RiverModel 的部分全部删去。

  【游戏效果图】

  Unity-游戏对象与图形基础 - 图10

  Unity-游戏对象与图形基础 - 图11

  【动态展示】

  🔗视频链接(新的游戏场景占用较多内存,游戏过程会有明显卡顿)

3、材料与渲染联系

  • Standard Shader 自然场景渲染器
  1. 选择合适内容,如 Albedo Color and Transparency,寻找合适素材,展示相关效果的呈现

  创建一个球体和一个 Material,将 Material 拖到球体上。首先调整颜色:

  Unity-游戏对象与图形基础 - 图12

  Unity-游戏对象与图形基础 - 图13

  通过调整 Alpha 值来控制其透明度:当 Rendering Mode 为 Opaque 时调整无效;当 Rendering Mode 为 Cutout 时,Alpha 值为 0~127 时材质为完全透明,而 Alpha 值为 128~255 时材质为完全不透明;当 Rendering Mode 为 Fade 时可以实现任意透明度;当 Rendering Mode 为 Transparent 时可以实现一定范围内的任意透明度:  Unity-游戏对象与图形基础 - 图14

  Unity-游戏对象与图形基础 - 图15

  Unity-游戏对象与图形基础 - 图16

  Unity-游戏对象与图形基础 - 图17

  调整 Metallic 值可以控制材质的金属感:

  Unity-游戏对象与图形基础 - 图18

  Unity-游戏对象与图形基础 - 图19

  (联想到了哈利波特的金色飞贼)

  调整 Smoothness 值可以控制材质的平滑度:

  Unity-游戏对象与图形基础 - 图20

  Unity-游戏对象与图形基础 - 图21

  (质感接近台球了)

  • 声音
  1. 给出游戏中利用 Reverb Zones 呈现车辆穿过隧道的声效的案例

  下载声音素材🔗Engine,在 Unity 场景中创建一个空对象,在该空对象上挂载组件 Audio Source 和 Audio Reverb Zone,将素材拖放到 Audio Source 的 AudioClip 作为声音资源,同时开启 Loop,并在 Audio Reverb Zone 中设置 Reverb Preset 为 Cave(隧道声效),运行游戏即可。

  Unity-游戏对象与图形基础 - 图22

  Unity-游戏对象与图形基础 - 图23

  🔗项目地址