3D Computer Game Programming-Note 4
1、基本操作演练
下载 Fantasy Skybox FREE,构建自己的游戏场景
从 Asset Store 上下载 Fantasy Skybox FREE:

导入包到 Assets 中:

在 Main Camera 上添加部件 Rendering -> Skybox,在已导入的 Fantasy Skybox FREE -> Materials 中选择喜欢的素材(.mat 文件)拖放到 Skybox 中:

运行:
- 在菜单栏选择 GameObject -> 3D Object -> Terrain,新建一个地形对象,同时为了更完整的视觉效果,在其附近创建另外三个地形对象:


- 在 Terrain 的 Inspector 窗口中选择合适的工具渲染起伏的地形、花草等,从 Asset Store 中导入水面预制,游戏场景最终效果如下:

- 写一个简单的总结,总结游戏对象的使用。
作为 Unity 中的基本对象,游戏对象可以作为组件的容器使用,即通过在游戏对象上挂载不同的组件来获得相关的属性,从而实现不同的功能。
比如,当希望得到一个视觉效果惊艳的游戏对象时,可以挂载 Material 调整颜色透明度等,或是挂载 Texture2D 为其贴上纹理;当希望一个游戏对象可以作为一个音频播放器时,可以在该游戏对象上挂载一个 Audio;当希望游戏对象可以在游戏运行时自动完成某些动作时,可以编写脚本并挂载到游戏对象上,在运行时自动执行脚本指定的逻辑实现。
游戏对象既可以通过直接创建的方式实例化,也可以通过预制来实例化。
2、实现《牧师与魔鬼》动作分离版
要求:设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束
本次游戏是在🔗牧师与恶魔的基础上将动作管理与游戏场景分离而实现的。借用课程主页的设计图:

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

为了实现动作分离,需要新增以下文件:
SSAction.cs: 继承 ScriptableObject(不需要绑定 GameObject 对象的可编程基类),作为游戏动作的基类
public class SSAction : ScriptableObject {public bool enable = true;public bool destory = false;public GameObject gameObject { get; set; }public Transform transform { get; set; }public ISSActionCallback callback { get; set; }protected SSAction() { }public virtual void Start() {throw new System.NotImplementedException();}public virtual void Update() {throw new System.NotImplementedException();}}
CCMoveToAction.cs: 实现具体动作,将一个物体移动到目标位置,并通知任务完成
public class CCMoveToAction : SSAction {public Vector3 target;public float speed;public static CCMoveToAction GetSSAction(Vector3 target, float speed) {CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();action.target = target;action.speed = speed;return action;}private CCMoveToAction() { }public override void Start() { }public override void Update() {if (this.gameObject == null || this.transform.localPosition == target){this.destory = true;this.callback.SSActionEvent(this);return;}this.transform.localPosition = Vector3.MoveTowards(this.transform.localPosition, target, speed * Time.deltaTime);}}
CCSequenceAction.cs: 实现一个动作组合序列,顺序播放动作。在本游戏中主要服务于角色上下船的运动轨迹(只有一个移动动作时角色会沿直线而非折线运动,出现穿过其他游戏对象的情况)
public class CCSequenceAction : SSAction, ISSActionCallback {public List<SSAction> sequence;public int repeat = -1;public int start = 0;public static CCSequenceAction GetSSAction(int repeat, int start, List<SSAction> sequence) {CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();action.repeat = repeat;action.sequence = sequence;action.start = start;return action;}public override void Start() {foreach (SSAction action in sequence) {action.gameObject = this.gameObject;action.transform = this.transform;action.callback = this;action.Start();}}public override void Update() {if (sequence.Count == 0)return;if (start < sequence.Count)sequence[start].Update();}public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competed, int Param = 0, string strParam = null, Object objectParam = null) {source.destory = false;this.start++;if (this.start >= sequence.Count) {this.start = 0;if (repeat > 0)repeat--;if (repeat == 0) {this.destory = true;this.callback.SSActionEvent(this);}}}void OnDestory() { }}
SSActionManager.cs: 作为动作对象管理器的基类,实现了所有动作的基本管理
public class SSActionManager : MonoBehaviour {private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();private List<SSAction> waitingAdd = new List<SSAction>();private List<int> waitingDelete = new List<int>();protected void Start() { }protected void Update() {foreach (SSAction ac in waitingAdd)actions[ac.GetInstanceID()] = ac;waitingAdd.Clear();foreach (KeyValuePair<int, SSAction> kv in actions) {SSAction ac = kv.Value;if (ac.destory) {waitingDelete.Add(ac.GetInstanceID());}else if (ac.enable) {ac.Update();}}foreach (int key in waitingDelete) {SSAction ac = actions[key];actions.Remove(key);Destroy(ac);}waitingDelete.Clear();}public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager) {action.gameObject = gameObject;action.transform = gameObject.transform;action.callback = manager;waitingAdd.Add(action);action.Start();}}
CCActionManager.cs: 封装游戏中的具体动作,提供接口供场景控制器调用,实现动作管理与游戏场景分离
public class CCActionManager : SSActionManager, ISSActionCallback {private bool isMoving = false;public CCMoveToAction MoveBoatAction;public CCSequenceAction MoveRoleAction;public FirstController sceneController;public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competed, int Param = 0, string strParam = null, Object objectParam = null) {isMoving = false;}protected new void Start() {sceneController = (FirstController)SSDirector.GetInstance().CurrentSceneController;sceneController.SetActionManager(this);}public bool GetIsMoving() {return isMoving;}public void MoveBoat(GameObject obj, Vector3 target, float speed) {if (isMoving) {return;}isMoving = true;MoveBoatAction = CCMoveToAction.GetSSAction(target, speed);this.RunAction(obj, MoveBoatAction, this);}public void MoveRole(GameObject role, Vector3 transfer, Vector3 target, float speed) {if (isMoving) {return;}isMoving = true;MoveRoleAction = CCSequenceAction.GetSSAction(0, 0, new List<SSAction> { CCMoveToAction.GetSSAction(transfer, speed), CCMoveToAction.GetSSAction(target, speed) });this.RunAction(role, MoveRoleAction, this);}}
ISSActionCallback.cs: 实现消息通知,避免与动作管理者直接依赖
public enum SSActionEventType : int { Started, Competed }public interface ISSActionCallback {void SSActionEvent(SSAction source,SSActionEventType events = SSActionEventType.Competed,int intParam = 0,string strParam = null,Object objectParam = null);}
Judge.cs: 裁判类,当游戏达到结束条件时,通知场景控制器游戏结束
public class Judge : MonoBehaviour {public FirstController sceneController;public CoastModel srcCoastModel;public CoastModel desCoastModel;public BoatModel boatModel;void Start() {sceneController = (FirstController)SSDirector.GetInstance().CurrentSceneController;srcCoastModel = sceneController.SrcCoastController.GetCoastModel();desCoastModel = sceneController.DesCoastController.GetCoastModel();boatModel = sceneController.boatController.GetBoatModel();}// 参考之前的 Check()void Update() {if (!sceneController.isRunning)return;if (sceneController.time <= 0) {sceneController.JudgeCallback("Game Over!", false);return;}this.gameObject.GetComponent<UserGUI>().result = "";if (desCoastModel.priestNum == 3) {sceneController.JudgeCallback("You Win!", false);return;}else {int leftPriestNum, leftDevilNum, rightPriestNum, rightDevilNum;leftPriestNum = srcCoastModel.priestNum + (boatModel.OnRight ? 0 : boatModel.priestNum);leftDevilNum = srcCoastModel.devilNum + (boatModel.OnRight ? 0 : boatModel.devilNum);if (leftPriestNum != 0 && leftPriestNum < leftDevilNum) {sceneController.JudgeCallback("Game Over!", false);return;}rightPriestNum = desCoastModel.priestNum + (boatModel.OnRight ? boatModel.priestNum : 0);rightDevilNum = desCoastModel.devilNum + (boatModel.OnRight ? boatModel.devilNum : 0);if (rightPriestNum != 0 && rightPriestNum < rightDevilNum) {sceneController.JudgeCallback("Game Over!", false);return;}}}}
同时,需要更改以下文件:
FirstController.cs: 主要更改与动作管理相关的部分(与上一版相同的部分代码略去)
public class FirstController : MonoBehaviour, ISceneController, IUserAction {/* 增加动作管理器 */public CCActionManager ActionManager;public CoastController DesCoastController;public CoastController SrcCoastController;public BoatController boatController;public RoleController[] roleModelControllers;//private MoveController moveController;private RiverModel river;public bool isRunning;public float time;public float speed = 8;public void SetActionManager(CCActionManager actionManager) {this.ActionManager = actionManager;}void Awake() {SSDirector director = SSDirector.GetInstance();director.CurrentSceneController = this;director.CurrentSceneController.LoadResources();this.gameObject.AddComponent<UserGUI>();this.gameObject.AddComponent<CCActionManager>();this.gameObject.AddComponent<Judge>();}public void LoadResources() {/*...*/}/* 使用动作管理器提供的接口实现运动,取代原来的 moveController;下同 */public void MoveBoat() {if ((!isRunning) || ActionManager.GetIsMoving())return;if (boatController.GetBoatModel().OnRight)ActionManager.MoveBoat(boatController.GetBoatModel().boat, PositionModel.boat_on_left, speed);elseActionManager.MoveBoat(boatController.GetBoatModel().boat, PositionModel.boat_on_right, speed);boatController.GetBoatModel().OnRight = !boatController.GetBoatModel().OnRight;}public void MoveRole(RoleModel roleModel) {if ((!isRunning) || ActionManager.GetIsMoving())return;Vector3 target, transfer;if (roleModel.OnBoat) {if (boatController.GetBoatModel().OnRight)target = DesCoastController.AddRole(roleModel);elsetarget = SrcCoastController.AddRole(roleModel);/* 设置一个中转点使运动轨迹成为折线;下同 */if (roleModel.role.transform.localPosition.y > target.y)transfer = new Vector3(target.x, roleModel.role.transform.localPosition.y, target.z);elsetransfer = new Vector3(roleModel.role.transform.localPosition.x, target.y, target.z);ActionManager.MoveRole(roleModel.role, transfer, target, 5);roleModel.OnRight = boatController.GetBoatModel().OnRight;boatController.RemoveRole(roleModel);}else {if (boatController.GetBoatModel().OnRight == roleModel.OnRight) {if (roleModel.OnRight) {DesCoastController.RemoveRole(roleModel);}else {SrcCoastController.RemoveRole(roleModel);}target = boatController.AddRole(roleModel);if (roleModel.role.transform.localPosition.y > target.y)transfer = new Vector3(target.x, roleModel.role.transform.localPosition.y, target.z);elsetransfer = new Vector3(roleModel.role.transform.localPosition.x, target.y, target.z);ActionManager.MoveRole(roleModel.role, transfer, target, 5);}}}public void Restart() {/*...*/}void Update() {if (isRunning) {time -= Time.deltaTime;this.gameObject.GetComponent<UserGUI>().time = (int)time;}}/* 将裁判类的返回信息呈现在游戏场景中 */public void JudgeCallback(string result, bool isRunning) {this.gameObject.GetComponent<UserGUI>().result = result;this.gameObject.GetComponent<UserGUI>().time = (int)time;this.isRunning = isRunning;}}
此外,由于增加了游戏场景,上一版中由长方体构成的“粗制滥造”的 River 已经不需要了,因此将所有文件中有关 RiverModel 的部分全部删去。
【游戏效果图】


【动态展示】
🔗视频链接(新的游戏场景占用较多内存,游戏过程会有明显卡顿)
3、材料与渲染联系
- Standard Shader 自然场景渲染器
选择合适内容,如 Albedo Color and Transparency,寻找合适素材,展示相关效果的呈现
创建一个球体和一个 Material,将 Material 拖到球体上。首先调整颜色:


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



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


(联想到了哈利波特的金色飞贼)
调整 Smoothness 值可以控制材质的平滑度:


(质感接近台球了)
- 声音
给出游戏中利用 Reverb Zones 呈现车辆穿过隧道的声效的案例
下载声音素材🔗Engine,在 Unity 场景中创建一个空对象,在该空对象上挂载组件 Audio Source 和 Audio Reverb Zone,将素材拖放到 Audio Source 的 AudioClip 作为声音资源,同时开启 Loop,并在 Audio Reverb Zone 中设置 Reverb Preset 为 Cave(隧道声效),运行游戏即可。


