目录
一、基本操作演练
1、下载 Fantasy Skybox FREE, 构建自己的游戏场景
-
下载天空盒 Fantasy Skybox FREE,它包含了一些天空贴图
将下载的天空盒文件导入
-
创建 Material(Assets 上下文菜单 -> create -> Material) 起名 mysky,在 Inspector 视图中选择 Shader -> Skybox -> 6Sided,并且将刚刚下载的天空盒图片依次加入到右面六张图片的空缺处,结果如图:
-
运行,就能够看到刚刚做好的天空盒
-
加上地形以及花草树木
2、写一个简单的总结,总结游戏对象的使用
Unity 游戏对象主要涉及三种类:
- GameObject: Unity 场景中所有实体的基类
- Component: 能附加到游戏对象的部件的基类
- Component 的各种子类。包括空间与变换部件 Transform、各种 渲染部件Reander ,脚本部件,MonoBehaviour 的子类等等。
游戏对象的使用:
- 所有游戏对象都有Active属性,Name属性,Tag属性等。每个 GameObject 还包含一个 transform 组件,我们可以通过这个组件来使游戏对象改变位置,旋转和缩放。
- 通过改变物体的 material ,我们可以赋予它们不同的材质或者说外观。
- 将 GameObject 做成预制是一个重要的游戏对象的使用方法,可以大大减少重复劳动的工作。将 游戏对象 拖动并放置在 Assets 面板之上,就生成了一个预制件
- 对 GameObject 来说最常用的添加功能的方法是添加组件(脚本),这样通过组合的方式既拓展了对象的功*能,还将组件与对象耦合度大大降低。
之前使用过的游戏对象:
-
3D 物体:Cube、Sphere、Capsule、Sylinder、Plane、Quad,由三角形网格构建的物体:如Terrain。
-
Empty (不显示却是最常用对象之一):作为子对象的容器或创建一个新的对象空间。
-
Camera 摄像机:观察游戏世界的窗口。
Projection属性包括正交视图与透视视图。Viewport Rect:属性是控制摄像机呈现结果对应到屏幕上的位置以及大小。屏幕坐标系:左下角是(0, 0),右上角是(1, 1)。Depth属性是当多个摄像机同时存在时,这个参数决定了呈现的先后顺序,值越大越靠后呈现。 -
light 光源
灯光类型(type)包括平行光(类似太阳光),聚光灯(spot),点光源(point),区域光(area,仅烘培用) -
audio 声音
将声音素材拖入摄像机就可以成为背景音乐。可以设置是否重复,音量等属性 -
skyboxes 天空盒
天空是一个球状贴图,通常用 6 面贴图表示。
使用方法有两步,第一为在摄像机添加天空组件。Component 加入skybox组件,第二为直接拖入天空材料(Material)。
二、编程实践
1、牧师与魔鬼 动作分离版
面向对象的游戏编程
如果感觉到场记(控制器)管的事太多,不仅要处理用户交互事件,还要游戏对象加载、游戏规则实现、运动实现等等,而显得非常臃肿。一个最直观的想法就是让更多的人(角色)来管理不同方面的工作。显然,这就是面向对象基于职责的思考,例如:用专用的对象来管理运动,专用的对象管理播放视频,专用的对象管理规则。就和踢足球一样,自己踢5人场,一个裁判就够了,如果是国际比赛,就需要主裁判、边裁判、电子裁判等角色通过消息协同来完成更为复杂的工作。
动作管理器的设计思想
为了用一组简单动作组合成复杂的动作,我们采用 cocos2d 的方案建立与 CCAtion 类似的类。设计思路如下:
- 设计一个抽象类作为游戏动作的基类
- 设计一个动作管理器类管理一组游戏动作的实现类
- 通过回调,实现动作完成时的通知
动作管理器的设计类图
动作管理器的代码与设计解读
-
SSAction (动作基类)
动作基类继承于
ScriptableObject
, 是不需要绑定 GameObject 对象的可编程基类。这些对象受 Unity 引擎场景管理。
动作基类中申明了Start
和Update
的虚函数,通过重写可以实现多态,这样继承者可以明确使用 Start 和 Update 编程游戏对象行为。
为了在完成动作后告知管理者,同时避免与动作管理者直接依赖,动作基类中定义了接口ISSACtionCallback
实现消息通知,避免与动作管理者直接依赖。
public class SSAction : ScriptableObject {
public bool enable = true; //允许动作发生
public bool destroy = false; //销毁动作
public GameObject gameobject { get; set; } //动作对象
public Transform transform { get; set; } //动作对象的 transform
public ISSActionCallback callback { get; set; } //动作管理者
//确保使用者不能够随意的创建动作
protected SSAction() { }
//子类需要对 Start 和 Update 进行重写
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
-
CCMoveToAction(简单动作)
实现将一个物体移动的目标位置,并通知
callback
对象任务已完成的功能。
一个易错点在于,需要加上对enable
的判断来决定是否需要移动物体,不能够让函数始终处于触发状态。因为当人物在跟随船移动的时候是不符合处于目标点的状态的,这样运行后会发现人物处于一个固定点,没有跟着船移动。
public class CCMoveToAction : SSAction {
public Vector3 target; //移动目标位置
public float speed; //移动速度
//函数 GetSSAction 返回一个设定好目标和移动速度的 action 对象
public static CCMoveToAction GetSSAction(Vector3 target, float speed) {
CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
action.target = target;
action.speed = speed;
return action;
}
//每次 Update 时物体进行少量的移动,为了平滑的移动到目标位置,需要由继承了Monobehavior类的对象来不断调用CCMoveToAction实例的Update函数
public override void Update() {
//一个易错点在于,需要加上对enable的判断来决定是否需要移动物体,不能够让函数始终处于触发状态。因为当人物在跟随船移动的时候是不符合处于目标点的状态的,这样运行后会发现人物没有跟着船移动。
if (enable) {
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed);
if (this.transform.position == target) {
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
public override void Start() {
Update();
}
}
-
SequenceAction (顺序动作组合类)
顺序动作是标准的组合设计模式,被组合的对象和组合对象属于同一种类型。通过组合模式,我们能实现几乎满足所有越位需要、非常复杂的动作管理。
在设计中需要让动作组合继承抽象动作,能够被进一步组合;实现回调接受,能接收被组合动作的事件;
创建一个动作顺序执行序列,-1 表示无限循环,start 开始动作。
Start
执行动作前,为每个动作注入当前动作游戏对象,并将自己作为动作事件的接收者
Update
方法执行执行当前动作
SSActionEvent
收到当前动作执行完成,推下一个动作,如果完成一次循环,减次数。如完成,通知该动作的管理者
OnDestory
如果自己被注销,应该释放自己管理的动作。
public class SequenceAction : SSAction, ISSActionCallback {
public List<SSAction> sequence;
public int repeat = -1; //循环次数、-1表示无限循环
public int start = 0; //进行到哪个动作的标识
public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence) {
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
action.repeat = repeat;
action.sequence = sequence;
action.start = start;
return action;
}
public override void Update() {
if (sequence.Count == 0) return;
if (start < sequence.Count) {
sequence[start].Update();
}
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed) {
source.destroy = false;
this.start++;
if (this.start >= sequence.Count) {
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0) {
this.destroy = true;
this.callback.SSActionEvent(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() {
//todo
}
}
- ISSActionCallback(动作事件接口)
public enum SSActionEventType : int { Started, Completed }
public interface ISSActionCallback {
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed);
}
-
SSActionManager(动作管理基类)
管理 SequenceAction 和 SSAction,可以给它们传递游戏对象,让游戏对象做动作或是一连串的动作,控制动作的切换。
SSActionManager 继承了 ISSActionCallback 接口,通过这个接口,当动作做完或是连续的动作做完时候会告诉 SSActionManager,然后 SSActionManager 去决定如何执行下一个动作。
public class SSActionManager : MonoBehaviour, ISSActionCallback {
//RunAction 函数为 Action 设定游戏对象以及动作结束后的通知者
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback callback) {
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = callback;
action.Start();
}public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed) {
}}
-
CCActionManager (动作管理)
在具体的动作管理中依照游戏要求需要实现两个动作,移动船只以及移动人物。
船只只包含平移的动作,所以使用简单的 CCMoveToAction 动作来执行;人物的动作由跳起、跃下,以及水平移动组成,所以需要使用顺序动作来控制。
public class CCActionManager : SSActionManager {
private CCMoveToAction moveBoat; //移动船只
private SequenceAction moveCharacter;public ISceneController controller; //移动人物
public void Start() {
controller = SSDirector.getInstance().currentSceneController;
controller.actionManager = this;
}
//动作管理器继承了 MonoBehaviour,而动作本身不具有在每个 tick更新状态的功能,所以管理器在每个自身 Update 被调用的时候要检查所管理的动作的状态,并调用相应的Update函数。
public void Update() {
if (moveBoat) moveBoat.Update();
if (moveCharacter) moveCharacter.Update();
}
public void MoveBoat(GameObject boat, Vector3 target, float speed) {
//获取以特定速度移动到 target 的动作对象
moveBoat = CCMoveToAction.GetSSAction(target, speed);
//将动作对象与船只对象绑定,让船只在动作的指挥下移动
RunAction(boat, moveBoat, this);
}
//使用顺序动作来控制人物的动作:跳起、跃下,以及水平移动
public void MoveCharacter(GameObject character, Vector3 target, float speed) {
Vector3 forward, up, down;
//设置跳起的水平距离,根据人物所处位置决定差值为正数或负数
float jumpDistance = target.x < 0 ? -8 : 8;
List<SSAction> sequence;
//人物跃下下方的船只
if (character.transform.position.y > target.y) {
forward = target;
forward.x += jumpDistance;
forward.y = character.transform.position.y;
down = target;
CCMoveToAction moveForward = CCMoveToAction.GetSSAction(forward, speed);
CCMoveToAction jump = CCMoveToAction.GetSSAction(down, speed);
//顺序动作由前进、跃下组成
sequence = new List<SSAction> { moveForward, jump };
}
//人物跳上上方的河岸
else {
forward = target;
up = character.transform.position;
up.x += jumpDistance;
up.y = target.y;
CCMoveToAction moveForward = CCMoveToAction.GetSSAction(forward, speed);
CCMoveToAction jump = CCMoveToAction.GetSSAction(up, speed);
//顺序动作由跳起、前进组成
sequence = new List<SSAction> { jump, moveForward };
}
//设定动作的执行次数为 1
moveCharacter = SequenceAction.GetSSAcition(1, 0, sequence);
RunAction(character, moveCharacter, this);
}}
裁判类的设计实现
- 通知 ISceneController 的接口设计
public enum GameStatus : int { win, lose, playing }
public interface ISSJudgeCallback {
void SSJudgeEvent(ISceneController source, GameStatus status = GameStatus.playing);
}
- 裁判类 Judge 的设计实现
public class Judge :MonoBehaviour, ISSJudgeCallback {
CharacterModel[] roles = new CharacterModel[6];
ISceneController callback;
BoatModel boat;int countOfDevil_1, countOfDevil_2, countOfPriest_1, countOfPriest_2;
public void setJudge(CharacterModel[] characters, BoatModel boat, ISceneController source) {
for(int i = 0; i < 6; i++) {
this.roles[i] = characters[i]; //裁判需要监视的对象
}
this.boat = boat;
callback = source;
}
public void SSJudgeEvent(ISceneController source, GameStatus status = GameStatus.playing) {
source.status = status; //告知 ISceneController 此时的游戏状态
}
public void Update() {
if (!boat.isSailing()) return; //在船行驶的时候才对游戏状态进行判断
if (callback.getGameStatus() == GameStatus.playing) {
countOfDevil_1 = countOfDevil_2 = countOfPriest_1 = countOfPriest_2 = 0;
//对两岸的游戏人物分别计数,查看是否符合游戏结束的条件
for (int i = 0; i < 6; i++) {
if (roles[i].getCoastName() == "leftCoast") {
if (roles[i].getType() == "priest") countOfPriest_1++;
else countOfDevil_1++;
}
else {
if (roles[i].getType() == "priest") countOfPriest_2++;
else countOfDevil_2++;
}
}
if (countOfDevil_1 > countOfPriest_1 && countOfPriest_1 > 0 || countOfDevil_2 > countOfPriest_2 && countOfPriest_2 > 0) {
SSJudgeEvent(callback, GameStatus.lose);
}
else if (countOfPriest_1 + countOfDevil_1 >= 6) {
SSJudgeEvent(callback, GameStatus.win);
}
}
}}
ISceneController 的重新设计
由于本次我们实现了将动作控制从 ISceneController 的工作中分离,所以需要对原先 ISceneController 直接控制船/人物模型的动作部分进行修改。
首先需要在 ISceneController 中添加成员 public CCActionManager actionManager
,这样就可以通过对actionManager 的操控间接控制动作发生,并且不需要了解太多细节。
- moveBoat
public void moveBoat() {
if (boat.isEmpty() || boat.isSailing()) return; //通过 actionManager 间接控制动作发生
actionManager.MoveBoat(boat.getBoat(), boat.getAndSetAnotherPort(), 0.50f);
for (int i = 0; i < 6; i++) {
if (characters[i].isOnBoat()) {
characters[i].setCoastName(boat.getCoastName());
}
}
}
- moveCharacter
public void moveCharacter(CharacterModel character) {
if (boat.isSailing()) return;
if (character.isOnBoat()) {
//character.setCoastName(boat.getCoastName());
character.leaveBoat();
boat.getOff(character.getSeatIndex());
Vector3 newPos;
int index;
if (character.getCoastName() == "leftCoast") {
index = leftCoast.getVacantIndex();
newPos = character.getCoastPosition(index);
newPos.x = -newPos.x;
}
else {
index = rightCoast.getVacantIndex();
newPos = character.getCoastPosition(index);
}
character.setPosIndexOnCoast(index);
actionManager.MoveCharacter(character.getCharacter(), newPos, 0.50f);
}
else {
if (boat.isFull() || boat.getCoastName() != character.getCoastName()) return;
int index = character.getPosIndexOnCoast();
if (character.getCoastName() == "leftCoast") leftCoast.getOff(character.getPosIndexOnCoast());
else rightCoast.getOff(character.getPosIndexOnCoast());
int seat = boat.getVacantIndex();
character.setSeat(seat);
actionManager.MoveCharacter(character.getCharacter(),boat.getBoat().transform.position +
character.getBoatPosition(seat), 0.50f);
character.board(boat.getBoat().transform);
}
}
三、材料与渲染联系
从 Unity 5 开始,使用新的 Standard Shader 作为自然场景的渲染。
阅读官方 Standard Shader 手册 。
选择合适内容,如 Albedo Color and Transparency,寻找合适素材,用博客展示相关效果的呈现
Albedo参数控制表面的基础色调
官网上由Albedo参数控制的物体颜色例子:
下面在Unity上做一个简单的练习。
- 首先通过 Assets -> create material
- 在新建的 material 的 inspector 面板上修改其 Albedo 值
- 新建一个 sphere 来展示刚刚做好的 material
- 将材料拖放到 sphere 上去
- 按照以上步骤制作五个不同 Albedo 的sphere
结果如下图,可以看到 Albedo 成功改变了物体表面颜色
A另外,lbedo 颜色的 alpha 通道控制材质的透明度程度。需要注意的是,这只对材质中的Rendering Mode为透明模式(Transparent、Fade)有效,而Opaque模式没有效果。
为上面的sphere添加透明度后效果如下:
官网上给出的 transparent 的使用样例很棒。镜头可以透过破损的窗口看到建筑物内。玻璃裂开的缝隙是完全透明的,而其余的部分是半透明的。