1、基本操作演练
要求
- 下载 Fantasy Skybox FREE, 构建自己的游戏场景
- 写一个简单的总结,总结游戏对象的使用
构建游戏场景
制作并添加skyBox
- 下载skybox
在 Store 中搜索Fantasy Skybox FREE,下载并导入Assert文件夹
资源包中有编辑好的skyBox00,再Textures下有构建天空图的图片
使用
- 在摄像机添加天空组件。Component → rendering → skybox
- 拖入天空材料(Material),结果
接下来我们构建一个自己的skyBox(制作一个 6 面体材料) - 菜单 Assets → create → Material → 命名为mySky
- Inspector → shader → skybox → 6 sided
- 按前后、上下、左右拖入 6 个图片
- 制作完成,拖入main Camear的skyBox插槽,运行结果
构建地形
地形设计工具
建地形
给地形上色,种草
添加预制里的模型,种树
Baking后的结果,
白色没有渲染到
游戏对象的使用总结
GameObject 中的最基础的游戏元素
- Empty (不显示却是最常用对象之一)
- 作为子对象的容器
- 创建一个新的对象空间
- 3D 物体:3D游戏中的重要组成部分,可以改变其网格和材质
- 基础 3D 物体(Primitive Object):立方体(Cube)、球体(Sphere)、胶囊体(Capsule)、圆柱体(Sylinder)、平面(Plane)、四边形(Quad)
- 构造 3D 物体:由三角形网格构建的物体:如地形等
- 2D物体:游戏中的二维物体
- Camera 摄像机,观察游戏世界的窗口
- Light 光线,游戏世界的光源,灯光定义了3D环境的颜色和情绪
- Audio 声音:3D游戏中的声音来源
- UI 基于事件的 new UI 系统
- Particle System 粒子系统与特效
游戏物体共有的属性
- Active:不活跃则不会执行 update() 和 rendering 等消息或事件
- Name:对象的名字,不是ID,不同对象可以重名。ID 使用 GetInstanceID()
- Tag:字串,有特殊用途。如标识主摄像机等
- Layer:[0…31],分组对象管理。常用于摄像机选择性渲染等
- transForm:空间属性
添加部件
2、编程实践
牧师与魔鬼 动作分离版
在上次游戏设计中,场记(控制器)不仅要处理用户交互事件,还要管理游戏对象的加载、船和人物的运动,实现游戏规则判断等,显得非常臃肿。一种改进的方法是用专用的对象来管理运动,专用的对象管理规则,专用的对象管理播放视频,从而面向对象基于职责进行思考设计。本次动作分离版的实现就是创建一个动作管理器来协调控制各个对象的控制器,实现游戏对象的运动管理,将原来FirstController对游戏对象的管理职责分离出来。
本次编程实践,基于老师课堂上介绍的动作管理器的设计方案,结合自己上次的项目,实现了SSActionManage,使得原来的FirstController可以通过SSActionManage完成游戏对象移动。
动作管理器的设计思路解析
- 设计一个抽象类作为游戏动作的基类
- 设计一个动作管理器类管理一组游戏动作的实现类
- 通过回调,实现动作完成时的通知
从而让程序可以方便的定义动作并实现动作的自由组合,使得: - 程序更能适应需求变化
- 对象更容易被复用
- 程序更易于维护
具体类的实现
- 动作基类(SSAction)
SSAction定义动作的基本属性enable,destory,并使用 virtual 申明虚方法,明确继承者编程游戏对象行为的基本方法。它继承了ScriptableObject 类,该类是不需要绑定 GameObject 对象的可编程基类。同时利用接口(ISSACtionCallback)实现消息通知,避免与动作管理者直接依赖。
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameobject;
public Transform transform;
public ISSActionCallback callback;
protected SSAction() {} //protected 防止用户自己 new 对象
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
- 简单动作实现
实现具体动作,将一个物体移动到目标位置,并通知任务完成。首先根据GetSSAction函数得到一个SSAction基类,然后unity创建一个动作类,并根据SSAction的属性设置自己内部的target、speed属性。动作类重写Start和Update函数实现多态,在Update中进行物体的移动,动作完成后,将destory设为true,期望管理程序自动回收运行对象,并发出事件通知管理者动作已完成。
public class SSMoveToAction : SSAction //移动
{
public Vector3 target;
public float speed;
private SSMoveToAction(){}
public static SSMoveToAction GetSSAction(Vector3 target, float speed)
{
SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();
action.target = target;
action.speed = speed;
return action;
}
public override void Update()
{
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed*Time.deltaTime);
if (this.transform.position == target)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start()
{
}
}
- 顺序组合动作实现
- 动作组合继承抽象动作,能够被进一步组合;实现回调接受,能接收被组合动作的事件
- 创建一个动作顺序执行序列,-1 表示无限循环,start 开始动作
- Start 执行动作前,为每个动作注入当前动作游戏对象,并将自己作为动作事件的接收者
- Update方法执行执行当前动作
- SSActionEvent 收到当前动作执行完成,推下一个动作,如果完成一次循环,减次数。如完成,通知该动作的管理者
- OnDestory 如果自己被注销,应该释放自己管理的动作
public class SequenceAction: SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -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.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
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()
{
}
}
- 动作事件接口定义
所有事件管理者(包括组合事件和事件管理器)都必须实现这个接口,实现消息通知
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);
}
- 动作管理基类:
- 动作对象管理器的基类,实现了所有动作的基本管理
- update中需要等待的动作加入等待列表,需要删除的动作加入删除列表。并进行动作的回收或运行后回收。
- RunAction。该方法把游戏对象与动作绑定,并绑定该动作事件的消息接收者
public class SSActionManager: MonoBehaviour, ISSActionCallback
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //将执行的动作的字典集合,int为key,SSAction为value
private List<SSAction> waitingAdd = new List<SSAction>(); //等待去执行的动作列表
private List<int> waitingDelete = new List<int>(); //等待删除的动作的key
protected void Update()
{
foreach(SSAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac; //获取动作实例的ID作为key
}
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);
}
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();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
}
}
进一步实现
FirstSceneActionManager
基于UML,设计FirstSceneActionManager,它继承ActionManager,封装了一些函数,使得FirstController调用起来更简洁。其中船的移动是一次直线移动,可通过SSMoveToAction创建简单事件完成。游戏中牧师和魔鬼的移动是折线运动,需要先通过一次直线运动到达中间点,再通过一次直线运动从中间点运动到目的地,因而需要通过SequenceAction将两次直线运动顺序组合起来。
public class FirstSceneActionManager : SSActionManager
{
private SSMoveToAction move_boat_action;
private SequenceAction move_role_action;
protected new void Start()
{
}
public void moveBoat(GameObject boat, Vector3 target, float speed)
{
move_boat_action = SSMoveToAction.GetSSAction(target, speed);
this.RunAction(boat, move_boat_action, this);
}
public void moveRole(GameObject role, Vector3 middle_pos, Vector3 end_pos, float speed)
{
SSAction action1 = SSMoveToAction.GetSSAction(middle_pos, speed);
SSAction action2 = SSMoveToAction.GetSSAction(end_pos, speed);
move_role_action = SequenceAction.GetSSAcition(1, 0, new List<SSAction> { action1, action2 });
this.RunAction(role, move_role_action, this);
}
}
BoatController和CharactersController将原来自己控制器的move动作交给FirstSceneActionManager管理,更改原来的移动动作,并提供方法告知FirstController移动目的
public class BoatController
{
public float move_speed = 20;//添加部分
//readonly Moveable moveableScript;//修改部分
/*public Vector3 Move()//删除部分
{
if (to_or_from == -1)
{
moveableScript.setDestination(fromPosition);
to_or_from = 1;
}
else
{
moveableScript.setDestination(toPosition);
to_or_from = -1;
}
}*/
public Vector3 getDestination()//添加部分
{
if (to_or_from == -1)
{
to_or_from = 1;
return fromPosition;
}
else
{
to_or_from = -1;
return toPosition;
}
}
}
修改FirstController,将原来的管理实现改为通过FirstSceneActionManager来管理
增加FirstSceneActionManager类并在Awake时添加,移动改为通过FirstSceneActionManager实现
修改后的FirstController代码
public class FirstController : MonoBehaviour, SceneController, UserAction
{
//...
private FirstSceneActionManager myActionManager;//添加动作管理器
void Awake()
{
Director director = Director.getInstance();
director.currentSceneController = this;
userGUI = gameObject.AddComponent<UserGUI>() as UserGUI;
characters = new MyCharacterController[6];
myActionManager = gameObject.AddComponent<FirstSceneActionManager>() as FirstSceneActionManager; //动作管理器部件获取
camera = GameObject.Find("Main Camera");
camera.transform.position = new Vector3(0,3,-15);
loadResources();
}
public void loadResources()
{
//...
}
public void moveBoat()
{
if (boat.isEmpty())
return;
//boat.Move(); //修改部分
myActionManager.moveBoat(boat.getGameobj(), boat.getDestination(), boat.move_speed);//修改部分
userGUI.status = check_game_over();
}
public void characterIsClicked(MyCharacterController characterCtrl)
{
if (characterCtrl.isOnBoat())
{
CoastController whichCoast;
if (boat.get_to_or_from() == -1)
{ // to->-1; from->1
whichCoast = toCoast;
}
else
{
whichCoast = fromCoast;
}
boat.GetOffBoat(characterCtrl.getName());
//characterCtrl.moveToPosition(whichCoast.getEmptyPosition()); //修改部分
Vector3 end_pos = whichCoast.getEmptyPosition(); //修改部分
Vector3 middle_pos = new Vector3(characterCtrl.getGameObj().transform.position.x, end_pos.y, end_pos.z); //修改部分
myActionManager.moveRole(characterCtrl.getGameObj(), middle_pos, end_pos, myActionManager.roleSpeed); //修改部分
whichCoast.getOnCoast(characterCtrl);
characterCtrl.getOnCoast(whichCoast);
}
else
{ // character on coast
CoastController whichCoast = characterCtrl.getCoastController();
if (boat.getEmptyIndex() == -1)
{ // boat is full
return;
}
if (whichCoast.get_to_or_from() != boat.get_to_or_from()) // boat is not on the side of character
return;
Vector3 end_pos = boat.getEmptyPosition(); //修改部分
Vector3 middle_pos = new Vector3(end_pos.x, characterCtrl.getGameObj().transform.position.y, end_pos.z); //修改部分
myActionManager.moveRole(characterCtrl.getGameObj(), middle_pos, end_pos, myActionManager.boatSpeed); //修改部分
whichCoast.getOffCoast(characterCtrl.getName());
//characterCtrl.moveToPosition(boat.getEmptyPosition());
characterCtrl.getOnBoat(boat);
boat.GetOnBoat(characterCtrl);
}
userGUI.status = check_game_over();
}
int check_game_over()
{
//...
}
public void restart()
{
//...
}
}
裁判类设计
【2019新要求】:设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束
将原来判断游戏输赢的任务分离出来,交给单独的一个类Judgment管理。需要传入相应的controller作为参数,帮助Judgmeng类判断。
public class Judgment : MonoBehaviour
{
public int check_game_over(BoatController boat, CoastController fromCoast, CoastController toCoast)
{ // 0->not finish, 1->lose, 2->win
int from_priest = 0;
int from_devil = 0;
int to_priest = 0;
int to_devil = 0;
int[] fromCount = fromCoast.getCharacterNum();
from_priest += fromCount[0];
from_devil += fromCount[1];
int[] toCount = toCoast.getCharacterNum();
to_priest += toCount[0];
to_devil += toCount[1];
if (to_priest + to_devil == 6) // win
return 2;
int[] boatCount = boat.getCharacterNum();
if (boat.get_to_or_from() == -1)
{ // boat at toCoast
to_priest += boatCount[0];
to_devil += boatCount[1];
}
else
{ // boat at fromCoast
from_priest += boatCount[0];
from_devil += boatCount[1];
}
if (from_priest < from_devil && from_priest > 0)
{ // lose
return 1;
}
if (to_priest < to_devil && to_priest > 0)
{
return 1;
}
return 0; // not finish
}
}
为FirstController添加Judgment类,并修改移动物体后判断是否游戏结束的语句
public class FirstController : MonoBehaviour, SceneController, UserAction
{
private Judgment judgment; //添加裁判
void Awake()
{
//...
judgment = gameObject.AddComponent<Judgment>() as Judgment; //裁判获取
}
userGUI.status = judgment.check_game_over(boat, fromCoast, toCoast);
}
效果
- 代码改变后,游戏功能未改变。场记FirstController不再负责船、牧师、魔鬼的具体移动,而是通过动作管理器具体移动游戏对象。
- 通过Judgment管理游戏结束的判定,实现裁判功能的分离。
- 可通过FirstSceneActionManager改变船和任务的移动速度