作业四
基本操作演练
构建游戏场景
我们首先在商店页面找到Fantasy Skybox FREE的天空盒,不过这里因为版本的问题没有办法进行使用,所以我们换了一个可以使用的天空盒。
使用天空盒的方法是将其添加到主摄像机里面,然后我们就可以在游戏界面看到如下的场景:
接下来是地形的构建,我们在GameObject的选项中选择3D Object,然后选择Terrain就可以构建出一个初始的地形。在地形设计工具中,Unity也给我们提供了相当多的工具供使用:
简单的介绍如下所示:
我们就可以根据这些工具来构建自己的地形了。
如果要构建树木和草地,我们还需要从商店中加载一些资源和贴图,最后利用地形刷将其放在地形上。可以在如下地方添加下载好的贴图资源:
我们还可以给这个场景加一些游戏对象,最终我们做好的场景如下:
开始运行后,也可以看见天空盒的效果,以及直升机螺旋桨的旋转和草丛的摆动:
对象的使用
游戏对象的定义如下:
GameObjects are the fundamental objects in Unity that represent characters, props and scenery. They do not accomplish much in themselves but they act as containers for Components, which implement the real functionality.
一个游戏对象代表了一种容器,每种对象都具有其最基本的属性,剩余的部分我们需要添加组件和脚本来进行构建,比如在脚本中定义其行为以及属性、动作等,在组件中改变其外貌、纹理等等,每种游戏对象是由它拥有的组件来决定它的功能的。
基本属性包括:
- layer——对象的层次
- tag——对象的标签
- scene——所属的场景
- activeSelf——本地激活状态
- isStatic——是否为静态
- activeInHierarchy——场景中是否活跃
- transform——附加到此对象的Transform组件
牧师与魔鬼 动作分离版
动作管理器的设计
为了用一组简单的动作组合成复杂的动作,我们采用 cocos2d 的方案,建立与 CCAtion 类似的类。其中,cocos2d 是一个基于MIT协议的开源框架,用于构建游戏、应用程序和其他图形界面交互应用。我们的设计思路如下:
- 设计一个抽象类作为游戏动作的基类
- 设计一个动作管理器类管理一组游戏动作的实现类
- 通过回调,实现动作完成时的通知
这样的目的是让程序可以方便的定义动作并实现动作的自由组合,使得:
- 程序更能适应需求变化
- 对象更容易被复用
- 程序更易于维护
UML类图如下所示:
核心代码与设计
动作事件接口定义
SSActionCallback
定义了事件处理接口,所有事件管理者都必须实现这个接口来实现事件调度。当动作完成的时候,动作会调用这个接口,发送消息告诉动作管理者对象这个动作已经完成,然后管理者会对下一个动作进行处理。代码如下所示,我们可以将其放在我们定义的命名空间Interfaces
下面:
public interface SSActionCallback
{
void SSActionCallback(SSAction source);
}
动作基类
SSAction
是所有动作管理的基类,它继承了ScriptableObject
,这代表它不需要绑定GameObject
对象,并且受Unity引擎场景管理。
(ScriptableObject 是不需要绑定 GameObject 对象的可编程基类)
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameObject;
public Transform transform;
public SSActionCallback CallBack;
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
动作管理基类
SSActionManager
是动作管理的基类,实现游戏对象与动作的绑定,确定回调函数消息的接收对象。
public class SSActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingToAdd = new List<SSAction>();
private List<int> watingToDelete = new List<int>();
protected void Update()
{
foreach (SSAction ac in waitingToAdd)
{
actions[ac.GetInstanceID()] = ac;
}
waitingToAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destroy)
watingToDelete.Add(ac.GetInstanceID());
else if (ac.enable)
ac.Update();
}
foreach (int key in watingToDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
watingToDelete.Clear();
}
public void addAction(GameObject gameObject, SSAction action, SSActionCallback ICallBack)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.CallBack = ICallBack;
waitingToAdd.Add(action);
action.Start();
}
}
简单动作实现
CCMoveToAction
实现了简单的动作,在本游戏中实现的是移动动作, 并管理内存回收。
public class CCMoveToAction : SSAction
{
public Vector3 target;
public float speed;
private CCMoveToAction() {}
public static CCMoveToAction getAction(Vector3 target, float speed)
{
CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
action.target = target;
action.speed = speed;
return action;
}
public override void Update()
{
this.transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime);
if (transform.position == target)
{
destroy = true;
CallBack.SSActionCallback(this);
}
}
public override void Start() {}
}
顺序动作组合类实现
CCSequenceAction
类实现了组合动作,其创建动作执行序列,按要求循环执行保存的动作序列。
public class CCSequenceAction : SSAction, SSActionCallback
{
public List<SSAction> sequence;
// 1 => repeat for once
//-1 => repeat forever
public int repeat = 1;
public int currentActionIndex = 0;
public static CCSequenceAction getAction(int repeat, int currentActionIndex, List<SSAction> sequence)
{
CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
action.sequence = sequence;
action.repeat = repeat;
action.currentActionIndex = currentActionIndex;
return action;
}
public override void Update()
{
if (sequence.Count == 0)
return;
if (currentActionIndex < sequence.Count)
sequence[currentActionIndex].Update();
}
public void SSActionCallback(SSAction source)
{
source.destroy = false;
this.currentActionIndex ++;
if (this.currentActionIndex >= sequence.Count)
{
this.currentActionIndex = 0;
if (repeat > 0)
repeat --;
if (repeat == 0)
{
this.destroy = true;
this.CallBack.SSActionCallback(this);
}
}
}
public override void Start()
{
foreach (SSAction action in sequence)
{
action.gameObject = this.gameObject;
action.transform = this.transform;
action.CallBack = this;
action.Start();
}
}
void OnDestroy()
{
foreach (SSAction action in sequence)
{
DestroyObject(action);
}
}
}
场景控制器
FirstSceneActionManager
是当前场景下的动作管理的具体实现,与场景控制基类配合,实现对当前场景的直接管理。
public class FirstSceneActionManager : SSActionManager, SSActionCallback
{
public SSActionEventType Complete = SSActionEventType.Completed;
public void BoatMove(BoatController Boat)
{
Complete = SSActionEventType.Started;
CCMoveToAction action = CCMoveToAction.getAction(Boat.GetDestination(), Boat.GetMoveSpeed());
addAction(Boat.GetGameObject(), action, this);
Boat.ChangeState();
}
public void GameObjectsMove(GameObjects GameObject, Vector3 Destination)
{
Complete = SSActionEventType.Started;
Vector3 CurrentPos = GameObject.GetPosition();
Vector3 MiddlePos = CurrentPos;
if (Destination.y > CurrentPos.y)
MiddlePos.y = Destination.y;
else
MiddlePos.x = Destination.x;
SSAction action1 = CCMoveToAction.getAction(MiddlePos, GameObject.GetMoveSpeed());
SSAction action2 = CCMoveToAction.getAction(Destination, GameObject.GetMoveSpeed());
SSAction seqAction = CCSequenceAction.getAction(1, 0, new List<SSAction> { action1, action2 });
this.addAction(GameObject.GetGameobject(), seqAction, this);
}
public void SSActionCallback(SSAction source)
{
Complete = SSActionEventType.Completed;
}
}
最后,我们还需要修改场记,应该使用动作分离的动作管理。比方说,在ObjectIsClicked
函数中,原先的代码如下:
if (Objects.isOnBoat())
{
Coast getCoast;
if (boat.getState() == -1)
getCoast = toCoast;
else
getCoast = fromCoast;
boat.GetOffBoat(Objects.getName());
Objects.moveToPosition(getCoast.getEmptyPosition());
Objects.getOnCoast(getCoast);
getCoast.getOnCoast(Objects);
}
我们应该重新利用FCActionManager
改写moveToPosition
,最终如下所示:
if (Objects.isOnBoat())
{
CoastController getCoast;
if (boat.get_State() == -1)
getCoast = toCoast;
else
getCoast = fromCoast;
boat.GetOffBoat(Objects.getName());
FCActionManager.GameObjectsMove(Objects, getCoast.getEmptyPosition());
Objects.getOnCoast(getCoast);
getCoast.getOnCoast(Objects);
}
增加裁判类
裁判类的作用是当游戏达到结束条件时,通知场景控制器游戏结束。也就是说我们需要将判断游戏结束与否的功能函数重新放进一个新的类里面去。
public class Judger
{
private BoatController boat;
private CoastController fromCoast;
private CoastController toCoast;
public Judger(CoastController fromCoast, CoastController toCoast, BoatController boat)
{
this.fromCoast = fromCoast;
this.toCoast = toCoast;
this.boat = boat;
}
public int GameState()
{
int state = Check();
return state;
}
private int Check()
{
int from_priest = 0;
int from_devil = 0;
int to_priest = 0;
int to_devil = 0;
int[] fromCount = fromCoast.GetobjectsNumber();
from_priest += fromCount[0];
from_devil += fromCount[1];
int[] toCount = toCoast.GetobjectsNumber();
to_priest += toCount[0];
to_devil += toCount[1];
// win
if (to_priest + to_devil == 6)
return 2;
int[] boatCount = boat.GetobjectsNumber();
// on destination
if (boat.get_State() == -1)
{
to_priest += boatCount[0];
to_devil += boatCount[1];
}
// at start
else
{
from_priest += boatCount[0];
from_devil += boatCount[1];
}
// lose
if ((from_priest < from_devil && from_priest > 0) || (to_priest < to_devil && to_priest > 0))
{
return 1;
}
// not finish
return 0;
}
}
最终,我们将改好的场记FirstController
和FirstSceneActionManager
一起挂载到空对象中,运行游戏即可:
不过这样似乎有些难看,我们可以利用天空盒给游戏场景增添一些色彩,同时我们可以将水、河岸、船的模型替换一下。在替换船的模型的时候,因为船分为了两头,从一边到另一边船头的方向是不一样的,所以我们还需要将船头改变方向,只需要加上一段简单的代码即可:
//change boat's direction
public void changeDirection()
{
if (needChangeDirection)
{
boat.transform.Rotate(0, 0, 180);
needChangeDirection = false;
}
}
最终的效果如下所示:
材料与渲染联系
自然场景渲染
从 Unity 5 开始,使用新的 Standard Shader 作为自然场景的渲染,这是一种物理渲染的方式。在一般的光照计算中,基本上是基于模拟的模型,即是尽可能的模拟我们看上去的物体反射的颜色,而基于物理的光照计算则是依据了光线传播的物理特性,使用一些近似计算,让材质更加贴近于真实情况,所以物理渲染在表现自然界的物体时会看上去更加真实。
其主要的特点包括:
- 节能:保证物体反射的光纤不会超过它们接受的光线。材质的镜面反射越强,漫反射就越少;曲面越平滑,高光就会越强越小。
- 高动态范围(HDR):颜色不再是0-1范围内的了,太阳的亮度现在可能会超过天空的亮度数十倍
我们选择Albedo Color and Transparency来演示相关的效果。我们首先选择一种玻璃材质的贴图贴在一个长方体上:
然后我们可以通过调节Albedo(反照率),改变物体表面的基本颜色,在物理模型中相当于物体表面某处各子表面的散射颜色,如下所示:
我们还可以改变其透明度,即是在Rendering Mode中设置模式后,改变参数α的值,可以逐渐看清藏在物体后面的小方块:
Unity 5 声音
Unity的音频特征包括全三维空间声音、实时混合和掌握、混合器的层次、快照、预定义效果等等。Reverb Zones即是混响区域,我们可以在Component->Audio->Audio Reverb Zones
中给一个物体添加混响区域:
其中包括三个基本属性:
- Min Distance:Represents the radius of the inner circle in the gizmo
, this determines the zone where there is a gradually reverb effect and a full reverb zone. - Max Distance:Represents the radius of the outer circle in the gizmo, this determines the zone where there is no effect and where the reverb starts to get applied gradually.
- Reverb Preset:Determines the reverb effect that will be used by the reverb zone.
在混响区中,声音的工作原理:
这里,我们将混响的属性调成Cave模式。
最后,我们在商店页面中导入免费的音频文件:
并创建一个Audio Source,将音频文件和其进行挂载,就可以在原来车声的基础上,模拟出车辆穿过隧道的声效了。