一、基本操作演练
1、下载 Fantasy Skybox FREE, 构建自己的游戏场景
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、牧师与魔鬼 动作分离版
**游戏视频 与 github 地址戳这里
面向对象的游戏编程
如果感觉到场记(控制器)管的事太多,不仅要处理用户交互事件,还要游戏对象加载、游戏规则实现、运动实现等等,而显得非常臃肿。一个最直观的想法就是让更多的人(角色)来管理不同方面的工作。显然,这就是面向对象基于职责的思考,例如:用专用的对象来管理运动,专用的对象管理播放视频,专用的对象管理规则。就和踢足球一样,自己踢5人场,一个裁判就够了,如果是国际比赛,就需要主裁判、边裁判、电子裁判等角色通过消息协同来完成更为复杂的工作。
动作管理器的设计思想
为了用一组简单动作组合成复杂的动作,我们采用 cocos2d 的方案建立与 CCAtion 类似的类。设计思路如下:
- 设计一个抽象类作为游戏动作的基类
- 设计一个动作管理器类管理一组游戏动作的实现类
- 通过回调,实现动作完成时的通知
动作管理器的设计类图
动作管理器的代码与设计解读
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();
}
}
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();
}
}
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);
}
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) {
}}
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);
}
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 的操控间接控制动作发生,并且不需要了解太多细节。
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());
}
}
}
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 的使用样例很棒。镜头可以透过破损的窗口看到建筑物内。玻璃裂开的缝隙是完全透明的,而其余的部分是半透明的。