这是中山大学2020年3D游戏设计的第四次作业,如有错误,请指正,感谢您的阅读。
在Asset中搜索Fantasy Skybox FREE,导入后直接使用
在 Camera 对象中添加部件 Rendering -> Skybox将天空盒拖放入 Skybox即可导入。这个过程将在编程训练中具体展现。
写一个简单的总结,总结游戏对象的使用首先我们应该知道游戏对象是所有其他组件的容器。游戏对象可以容纳很多组件,比如Transform,我们可以改变Transform的各个参数的值来改变游戏对象的位置。改变游戏对象的状态其实就是对游戏对象身上的组件进行更改。获得一个游戏对象的一个组件我们可以使用GetComponent方法,然后对组件的值进行修改。用AddComponent方法,在游戏对象上添加一个组件。 游戏对象也有自己的属性,可以利用这些属性比如可以使用Find方法将Name的游戏对象。 最后可以实例化游戏对象用Instantiate方法让它出现在场景中,也可以使用Destroy方法让游戏对象销毁消失在场景中。
牧师与魔鬼 动作分离版 设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。
最终,程序员可以方便的定义动作并实现动作的自由组合,做到:
程序更能适应需求变化对象更容易被复用程序更易于维护本次代码分析中有部分引自老师上课的讲义,特此声明。
设计要点:
ScriptableObject 是不需要绑定 GameObject 对象的可编程基类。这些对象受 Unity 引擎场景管理protected 防止用户自己 new 抽象的对象使用 virtual 申明虚方法,通过重写实现多态。这样继承者就明确使用 Start 和 Update 编程游戏对象行为利用接口ISSACtionCallback实现消息通知,避免与动作管理者直接依赖 using System.Collections; using System.Collections.Generic; using UnityEngine; public class SSAction : ScriptableObject { public bool enable = false; public bool destroy = 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(); } }这个模块实现具体动作,将一个物体移动到目标位置,并通知任务完成。 设计要点:
让 Unity 创建动作类,确保内存正确回收。别指望开发者是 c 语言高手。多态。C++ 语言必申明重写,Java则默认重写似曾相识的运动代码。动作完成,并发出事件通知,期望管理程序自动回收运行对象。 using System.Collections; using System.Collections.Generic; using UnityEngine; 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; Debug.Log(speed.ToString()); return action; } public override void Update() { this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime); if (this.transform.position == target) { this.enable = false; this.callback.SSActionEvent(this); } } }该部分定义接口作为接收通知对象的抽象类型.
设计要点:
事件类型定义,使用了枚举变量定义了事件处理接口,所有事件管理者都必须实现这个接口,来实现事件调度。所以,组合事件需要实现它,事件管理器也必须实现它。这里展示了语言函数默认参数的写法。 using System.Collections; using System.Collections.Generic; using UnityEngine; public enum SSActionEventType : int { Started, Competeted } public interface ISSActionCallback { void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null); }这是动作对象管理器的基类,实现了所有动作的基本管理。 设计要点:
创建 MonoBehaiviour 管理一个动作集合,动作做完自动回收动作。 有一个动作字典。待加入、删除动作列表。 update 演示了由添加、删除等复杂集合对象的使用。提供了添加新动作的方法 RunAction。该方法把游戏对象与动作绑定,并绑定该动作事件的消息接收者。执行该动作的 Start 方法 using System.Collections; using System.Collections.Generic; using UnityEngine; 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>(); // Use this for initialization protected void Start() { } // Update is called once per frame 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.destroy) { waitingDelete.Add(ac.GetInstanceID()); } else if (ac.enable) { ac.Update(); } } foreach (int key in waitingDelete) { SSAction ac = actions[key]; actions.Remove(key); DestroyObject(ac); } } public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) { action.gameobject = gameobject; action.transform = gameobject.transform; action.callback = manager; waitingAdd.Add(action); action.Start(); } }部署:
部署船向左移和向右移的动作和每个人物对应的上下船的动作启动所有部署的动作 using System.Collections; using System.Collections.Generic; using UnityEngine; public class CCActionManager : SSActionManager, ISSActionCallback { public FirstSceneControl sceneController; public CCMoveToAction moveToLeft, moveToRight; public Dictionary<int, CCOn_OffAction> on_off = new Dictionary<int, CCOn_OffAction>(); public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null) { } protected new void Start() { float speed = 10f; sceneController = (FirstSceneControl)Director.getInstance().currentSceneControl; sceneController.actionManager = this; moveToLeft = CCMoveToAction.GetSSAction(sceneController.Boat_Left, speed); moveToRight = CCMoveToAction.GetSSAction(sceneController.Boat_Right, speed); foreach (KeyValuePair<int, GameObject> obj in sceneController.On_Shore_r) { on_off[obj.Key] = CCOn_OffAction.GetSSAction(); } this.RunAction(sceneController.boat, moveToLeft, this); this.RunAction(sceneController.boat, moveToRight, this); foreach (KeyValuePair<int, GameObject> obj in sceneController.On_Shore_r) { this.RunAction(obj.Value, on_off[obj.Key], this); } } }控制器与上次的设计差不多,这里的阐述跟上次也基本相同。
Director是最高层的控制器,运行游戏时始终只有一个实例,它控制场景的加载、切换,游戏进程状态等等,可以说,是控制器的控制器。 我们要想让游戏顺利的运行,最重要的就是控制住只有一个Director实例,因为只有这样,我们才可以保证所有的脚本中获得的都是同一个Director对象,这样,也就能通过Director进行类之间的通信。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Director : System.Object { public MySceneControl currentSceneControl { get; set; } private static Director director; public static Director getInstance() { if (director == null) { director = new Director(); } return director; } }SceneController接口主要是Director控制场景控制器的渠道,但是在这里,只是一个接口,后续会用一个类来继承它并给出具体方法。
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface MySceneControl { void GenGameObjects(); }IUserAction的作用是给用户GUI提供控制接口,我们需要后台判断游戏的状态,需要获取剩余时间,同时还需要移动小船。
using System.Collections; using System.Collections.Generic; using UnityEngine; public enum GameState { WIN, FAILED, NOT_ENDED,TIME_UP } public interface IUserAction { void MoveBoat(); GameState getGameState(); float Gettime(); void Restart(); }On_off是角色移动的控制器,主要包括上下船,上下岸的过程。这个控制器主要接受的是点击的信号,只有点击角色时才会触发。 我们这里利用刚刚定义的动作组合实现。
using System; using System.Collections; using System.Collections.Generic; using UnityEngine; [DisallowMultipleComponent] public class On_Off : MonoBehaviour { private FirstSceneControl firstSceneControl; void Start() { firstSceneControl = (FirstSceneControl)Director.getInstance().currentSceneControl; } private void OnMouseDown() { int id = Convert.ToInt32(this.name); Debug.Log(id); if (firstSceneControl.b_state != Judge.BoatState.MOVING) { if (firstSceneControl.actionManager.on_off.ContainsKey(id)) { Debug.Log(firstSceneControl.actionManager.on_off[id].enable); firstSceneControl.actionManager.on_off[id].enable = true; } if (firstSceneControl.actionManager.on_off.ContainsKey(id - 6)) firstSceneControl.actionManager.on_off[id - 6].enable = true; } } }裁判类需要传入的参数有:在船上的物体,在两岸的物体,船的状态以及时间。而这里的实现方式与上次也完全相同,我们利用check()函数判断当前的游戏状态。 我们首先介绍构造函数,构造函数只需要将传入的值进行一一赋值即可。
public Judge(Dictionary<int, GameObject> _On_Boat, Dictionary<int, GameObject> _On_Shore_r, Dictionary<int, GameObject> _On_Shore_l, BoatState _b_state, float _timer) { On_Boat = _On_Boat; On_Shore_r = _On_Shore_r; On_Shore_l = _On_Shore_l; b_state = _b_state; timer = _timer; }下面是基本与上次一模一样的check()函数,时间的处理有所改变。
public GameState check() { if (On_Shore_l.Count == 6&&timer>0f) { return GameState.WIN; } else if (b_state == BoatState.STOPLEFT && timer > 0f) { if (get_num(On_Boat, 1) + get_num(On_Shore_l, 1) != 0 && get_num(On_Boat, 1) + get_num(On_Shore_l, 1) < (get_num(On_Boat, -1) + get_num(On_Shore_l, -1))) { //牧师不为0,且牧师数小于恶魔数 return GameState.FAILED; } if (get_num(On_Shore_r, 1) != 0 && get_num(On_Shore_r, 1) < get_num(On_Shore_r, -1)) { //这步是为了修正对岸的情况,以免出现卡bug情况 return GameState.FAILED; } } else if (b_state == BoatState.STOPRIGHT && timer > 0f) { if (get_num(On_Boat, 1) + get_num(On_Shore_r, 1) != 0 && get_num(On_Boat, 1) + get_num(On_Shore_r, 1) < (get_num(On_Boat, -1) + get_num(On_Shore_r, -1))) { //牧师不为0,且牧师数小于恶魔数 return GameState.FAILED; } if (get_num(On_Shore_l, 1) != 0 && get_num(On_Shore_l, 1) < get_num(On_Shore_l, -1)) { //这步是为了修正对岸的情况,以免出现卡bug情况 return GameState.FAILED; } } else if (timer <= 0f) return GameState.TIME_UP; else { return GameState.NOT_ENDED; } return GameState.NOT_ENDED; }check()中的get_num()函数与上次相同。
public int get_num(Dictionary<int, GameObject> dict, int ch) { var keys = dict.Keys; int d_num = 0; int p_num = 0; foreach (int i in keys) { if (i < 3 || (i >= 6 && i <= 8)) { p_num++; } else { d_num++; } } return (ch == 1 ? p_num : d_num); }至此,完成了Judge裁判类。
FirstSceneControl中部分与上次基本相同(Awake,Gettime,getGameState,GenGameObjects),这里仅介绍与上次有大幅度修改的函数。
Update函数需要调用Judge类来进行状态判断,返回该状态并进一步判断船的状态,最后进行位置修正。
private void Update() { Judge newjudge = new Judge(On_Boat, On_Shore_r, On_Shore_l, b_state, timer); game_state = newjudge.check(); if (timer > 0 && game_state == GameState.NOT_ENDED) timer -= Time.deltaTime * 10; if (game_state == GameState.NOT_ENDED)//判断状态 { if (boat.transform.position == Boat_Left) { b_state = Judge.BoatState.STOPLEFT; } else if (boat.transform.position == Boat_Right) { b_state = Judge.BoatState.STOPRIGHT; } else { b_state = Judge.BoatState.MOVING; } for (int i = 0; i < 6; i++) { if (On_Shore_l.ContainsKey(i)) On_Shore_l[i].transform.position = new Vector3(-12.5f - i * gab, -7.5f, 0); if (On_Shore_r.ContainsKey(i)) On_Shore_r[i].transform.position = new Vector3(12.5f + i * gab, -7.5f, 0);//位置修正 } int signed = 1; for (int i = 6; i < 12; i++) { if (On_Boat.ContainsKey(i))//在船上的角色位置修正 { On_Boat[i].transform.localPosition = new Vector3(signed * 0.3f, 1, 0); signed = -signed; } } } }MoveBoat比以前简化了很多,直接调用动作管理器进行移动即可。
public void MoveBoat() { if (On_Boat.Count != 0) { if (b_state == Judge.BoatState.STOPLEFT) { actionManager.moveToRight.enable = true; } if (b_state == Judge.BoatState.STOPRIGHT) { actionManager.moveToLeft.enable = true; } } }Restart需要将所有的物体回归原位,并且重置时间。注意要将物体回归原处而不是重新新建,否则会造成物体找不到动作管理器而不工作。
public void Restart() { foreach (var item in On_Boat.ToList()) { int id = Convert.ToInt32(On_Boat[item.Key].name)-6; Debug.Log("id:" + id); On_Shore_r.Add(id,On_Boat[id+6]); On_Shore_r[id].name = id.ToString(); On_Boat[item.Key].transform.parent = null; On_Boat.Remove(id + 6); } foreach (var item in On_Shore_l.ToList()) { int id = Convert.ToInt32(On_Shore_l[item.Key].name); On_Shore_r.Add(id, On_Shore_l[id]); On_Shore_l.Remove(id); } boat_capicity = 2;//船上只能有两个人 b_state = Judge.BoatState.STOPLEFT; boat.transform.position = Boat_Right; timer = 60f; game_state = GameState.NOT_ENDED; }UI与上次基本相同,只是增加了一个Restart按钮,如下:
if (GUI.Button(new Rect(160, 350, 100, 80), "restart!") && action.getGameState() != GameState.NOT_ENDED) { action.Restart(); }在Asset Store中下载Fantasy Skybox FREE,下载完成后,导入到自己的项目中,在该资源包中已经有一些做好的天空盒,我们在主相机中找到Camera属性,然后将Clear Flags设为Skybox,然后新建属性选择SkyBox,将一个资源拖入即可。因为这里我们使用的是2D视角展示游戏,故直接使用包中自带的天空盒即可,如图所示:
如图,时间加速了十倍来更好的进行展示。
本游戏使用了Asset Store中的:assets,Skybox
本次魔鬼与牧师动作分离版的关键在于动作的分离,因此我们需要动作管理器。在动作管理器中,因为我们要管理多个动作,所以,我们要对每个动作都进行override start的初始化工作(即使它的start是空的也要写上去!!),我就因为一个start是空所以省略不写导致debug几小时。其次,要注意unity的报错,在unity中,如果同时打开两个或者一个保持时间过长会有可能报错Asset database transaction committed twice!等等一系列的错误,这时,需要重启一下unity即可解决。 最后来说一下游戏,我认为这次最难的应该是类与类之间的交互关系,因为把动作分离,所以我们需要及时的调整变量以控制动作的及时触发。同时,这次增加了Restart函数,一开始我是将所有对象销毁然后重新生成,会发现这样并不能将动作管理加载到新的对象上,因此,采用将所有对象回归原位的方法进行重置,发现这样会有很好的效果。 总的来说,本次实验的重点在于如何使用动作管理将所有动作分离出去,而这一方法也让我更好的理解了面向对象的编程思想。
魔鬼与牧师动作分离版
透明度 反照率颜色的 Alpha 值控制着材质的透明度级别。仅当材质的 Rendering Mode(渲染模式)设置为 Opaque 之外的 Transparent 模式之一时,此设置才有效。如上所述,选择正确的透明度模式非常重要,因为此模式可确定您是否仍然会看到处于全值状态的反射和镜面高光,或它们是否也会根据透明度值淡出。
更多介绍请看:unity官方教程 我们使用unity来测试一下透明度和反照率。
我们新建五个material,将其mode设置为transparent,然后Albedo设置为等比数列的形式(0,51,102,153,204,255)五种。新建五个球体,将material分别赋给五个球,结果如下图所示。以上就是 Albedo Color and Transparency 的简单使用了。
阅读官方 Audio 手册 用博客给出游戏中利用 Reverb Zones 呈现车辆穿过隧道的声效的案例
首先,我们加入 Audio Reverb Zone 和 Audio Source 。 然后我们将Audio Reverb Zone中的Reverb Preset设置为Cave,如下所示 最后加入声音资源即可。