unity实现牧师与魔鬼2.0(动作分离版)& 基本操作演练 & 材料与渲染联系
基本操作演练
下载 Fantasy Skybox FREE, 构建自己的游戏场景
在Window->Asset Store 中下载Fantasy Skybox FREE,并将其全部import进项目中。然后在场景栏中右键3D Object->Terrain创建地形,点击地形,在Inspector栏中选择Paint Terrain,点击Paint Texture,然后选择Edit Terrain Layers,点击Add Layer。将相应的地形贴图添加。
然后选择 Raise or Lower Terrain对地形进行改变,Smooth Height可以对地形进行光滑处理。
地形处理好后,接下来要给地面上种一些草。选择Paint Details中的Edit Details,选择合适的草。
添加完成后,就可以往地面上种草了。
为了让场景更加生动,还需要添加天空盒,首先选中摄像头,然后在上方的菜单栏中选择Component->Rendering->Skybox。于是在该摄像头上就会看到一个Skybox组件,选择相应的天空盒贴图即可。
最终场景完成如下:
写一个简单的总结,总结游戏对象的使用
游戏对象是游戏中的基本单位,不仅包括2D对象和3D对象,还包括了摄像头和光源。每一个游戏对象都有自己的属性,有自己的位置。可以通过C#脚本来控制游戏对象的移动、创建和销毁。
编程实践
牧师与魔鬼 动作分离版
在上一版牧师与魔鬼的基础上,将游戏对象中的move组件分离出来,通过一个专门的动作管理器管理所有动作。区分与每个游戏对象都拥有一个move脚本,该版本的牧师与魔鬼通过场景控制器来专门控制游戏对象的移动。另外根据要求还需要设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。
UML类图如下:
将所有动作管理类合并成一个C#文件,所有文件如下:
代码分析
- SSAction(动作基类)
public class SSAction : ScriptableObject
{
public bool enable = true;
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();
}
}
动作基类,其它所有的动作都继承于该类。其继承于ScriptableObject类,代表不需要绑定 GameObject 对象的可编程基类,这些对象受 Unity 引擎场景管理。
- CCMoveToAction(管理移动动作的实现)
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;
return action;
}
public override void Update()
{
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
if (this.transform.position == target)
{
//waiting for destroy
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start(){}
}
通过设置速度和目的地,使得游戏对象可以以一定的速度向目的地移动
- CCSequenceAction(顺序动作组合类实现)
public class CCSequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -1;
public int start = 0;
public static CCSequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence)
{
CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
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()
{
//TODO: something
}
}
实现一个动作组合序列,顺序播放动作,通过继承SSAction类和ISSActionCallback类,使得游戏对象完成一个动作时可以去处理下一个动作。通过设置SSActionEvent()函数,则当前动作执行完成时,推下一个动作,如果完成一次循环,减次数。当次数减为0时,通知动作管理器动作已完成。
- ISSActionCallback(动作事件接口)
public interface ISSActionCallback
{
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
通过该接口,当动作完成时,动作会调用该接口使得动作管理器去完成下一个动作。
- SSActionManager(动作管理基类)
public class SSActionManager : MonoBehaviour, ISSActionCallback //action管理器
{
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
public int Moving = 0;
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();
Moving++;
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
Moving--;
}
}
管理所有基本动作,由于其继承了ISSActionCallback接口,使得一个动作完成时,可以告知动作管理器去处理下一个动作。
public class MySceneActionManager : SSActionManager
{
private CCMoveToAction moveBoatToEndOrStart;
private CCSequenceAction moveRoleToBank;
private CCSequenceAction moveRoleToBoat;
public Controll sceneController;
protected new void Start()
{
sceneController = (Controll)SSDirector.GetInstance().CurrentScenceController;
sceneController.actionManager = this;
}
public void moveBoat(GameObject boat, Vector3 target, float speed)
{
moveBoatToEndOrStart = CCMoveToAction.GetSSAction(target, speed);
this.RunAction(boat, moveBoatToEndOrStart, this);
}
public void move_boat(Boat boat, Vector3 pos){
moveBoatToEndOrStart = CCMoveToAction.GetSSAction(pos, 3);
this.RunAction(boat.getGameObject(), moveBoatToEndOrStart, this);
for(int i=0;i<2;++i){
if(boat.roles[i] != null){
Vector3 p = boat.roles[i].GetRolePos();
move_role_to_bank(boat.roles[i].getGameObject(),new Vector3(-p.x + 2*i-1, p.y, p.z));
}
}
}
public void move_role_to_boat(GameObject role,Vector3 pos){
Vector3 pos2 = new Vector3(role.transform.position.x, role.transform.position.y + 0.25f, role.transform.position.z);
Vector3 pos1 = new Vector3(pos.x, pos2.y, role.transform.position.z);
SSAction action1 = CCMoveToAction.GetSSAction(pos2, 3);
SSAction action2 = CCMoveToAction.GetSSAction(pos1, 3);
moveRoleToBoat = CCSequenceAction.GetSSAcition(1,0,new List<SSAction> {action1,action2});
this.RunAction(role, moveRoleToBoat, this);
}
public void move_role_to_bank(GameObject role,Vector3 pos){
SSAction action1 = CCMoveToAction.GetSSAction(pos, 3);
moveRoleToBank = CCSequenceAction.GetSSAcition(1,0,new List<SSAction> {action1});
this.RunAction(role, moveRoleToBank, this);
}
}
管理角色和船的移动,其中角色的移动分为从岸到船和从船到岸。前者需要两个SSMoveToAction动作组合,而后者和船的移动一样只需要一个SSMoveToAction。
- Controll
修改过后,由于游戏对象不再通过自身的move脚本来移动,而是统一通过动作管理器来控制移动,所以部分修改代码如下:
public MySceneActionManager actionManager;
public Judge judge;
void Start()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
User = gameObject.AddComponent<UserGUI>() as UserGUI;
LoadResources();
actionManager = gameObject.AddComponent<MySceneActionManager>() as MySceneActionManager;
judge = gameObject.AddComponent<Judge>() as Judge;
}
public void MoveBoat(){
if(boat.IsEmpty())return;
if(actionManager.Moving > 0)return;
actionManager.move_boat(boat,boat.BoatMoveToPosition());
User.check = judge.check();
}
public void MoveRole(Role role){
if(role.GetOnBank() == -boat.Get_boat_bank()) return;
if(actionManager.Moving > 0)return;
Vector3 pos;
if(role.GetOnBank() == 1 || role.GetOnBank() == -1){ //move role to boat
int temp = 0;
temp = boat.SetRoles(role); //返回一个空位置,若没有则返回-1
if(temp == -1)return; //boat is full
pos = boat.GetBoatPosition();
if(temp == 1 && role.GetOnBank() == 1) pos.x += 1;
if(temp == 0 && role.GetOnBank() == -1) pos.x -= 1;
//role.RoleMove(pos);
actionManager.move_role_to_boat(role.getGameObject(),pos); //动作分离版本
if(role.GetOnBank() == 1) right_bank.DeleteRole(role);
else left_bank.DeleteRole(role);
role.SetOnBank(2);
}
else if(role.GetOnBank() == 2){ //move role to bank
if(boat.Get_boat_bank() == 1){
pos = right_bank.GetPos();
role.SetOnBank(1);
right_bank.AddRole(role);
boat.DeleteRole(role);
//role.RoleMove2(pos);
actionManager.move_role_to_bank(role.getGameObject(),pos);//动作分离版本
}
else {
pos = left_bank.GetPos();
role.SetOnBank(-1);
left_bank.AddRole(role);
boat.DeleteRole(role);
//role.RoleMove2(pos);
actionManager.move_role_to_bank(role.getGameObject(),pos);//动作分离版本
}
User.check = judge.check();
}
}
裁判类的实现
由于需要设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。所以将原先控制器中的check()函数用一个专门的裁判类替换,代替控制器中的check()函数。
public class Judge : MonoBehaviour{
public Controll mainController;
public Boat boat;
public Bank left_bank;
public Bank right_bank;
public MySceneActionManager actionManager;
void Start()
{
mainController = (Controll)SSDirector.GetInstance().CurrentScenceController;
this.boat = mainController.boat;
this.left_bank = mainController.left_bank;
this.right_bank = mainController.right_bank;
this.actionManager = mainController.actionManager;
}
public int check(){
if(left_bank.GetPriestSum() == 3 && left_bank.GetDevilSum() == 3)return 1;
if(boat.Get_boat_bank() == -1){
if((left_bank.GetDevilSum() + boat.GetDevilSum() > left_bank.GetPriestSum() + boat.GetPriestSum()) && (left_bank.GetPriestSum() + boat.GetPriestSum()) != 0 && actionManager.Moving > 0)return 2;
else if((left_bank.GetDevilSum() + boat.GetDevilSum() > left_bank.GetPriestSum() + boat.GetPriestSum()) && (left_bank.GetPriestSum() + boat.GetPriestSum()) != 0 && actionManager.Moving == 0)return 3;
if(right_bank.GetDevilSum() > right_bank.GetPriestSum() && right_bank.GetPriestSum() != 0 && actionManager.Moving > 0) return 2;
else if(right_bank.GetDevilSum() > right_bank.GetPriestSum() && right_bank.GetPriestSum() != 0 && actionManager.Moving == 0) return 3;
}
else if(boat.Get_boat_bank() == 1){
if((right_bank.GetDevilSum() + boat.GetDevilSum() > right_bank.GetPriestSum() + boat.GetPriestSum()) && (right_bank.GetPriestSum() + boat.GetPriestSum()) != 0 && actionManager.Moving > 0)return 2;
else if((right_bank.GetDevilSum() + boat.GetDevilSum() > right_bank.GetPriestSum() + boat.GetPriestSum()) && (right_bank.GetPriestSum() + boat.GetPriestSum()) != 0 && actionManager.Moving == 0)return 3;
if(left_bank.GetDevilSum() > left_bank.GetPriestSum() && left_bank.GetPriestSum() != 0 && actionManager.Moving > 0) return 2;
else if(left_bank.GetDevilSum() > left_bank.GetPriestSum() && left_bank.GetPriestSum() != 0 && actionManager.Moving == 0) return 3;
}
return 0;
}
}
裁判类继承了MonoBehaviour。MonoBehaviour是每个脚本的基类,又继承于Behaviours。只有继承了MonoBehaviour的类才可以作为组件挂载到游戏对象上,在unity 中所有继承MonoBehaviour的类是不可以实例化的,因为unity都会自动为其创建实例,所以我们需要调用该类的时候不使用new,而是使用AddComponent来调用。
因此,通过在需要用到check函数的类里面声明一个裁判类并将其添加到游戏对象中就可以调用里面的check方法。实现对游戏结果的判断。
项目文件和代码
材料与渲染联系【可选】
- Standard Shader 自然场景渲染器
阅读官方 Standard Shader 手册 。
选择合适内容,如 Albedo Color and Transparency,寻找合适素材,用博客展示相关效果的呈现
官方Standard Shader手册
新创建一个球,一个材料。设置材料的Rendering Mode为Transparent
通过调色板可以调整材料的颜色和透明度
通过调节材料的Metallic和Smoothness属性,可以分别改变物体的金属参数和平滑度。
最后,将材料拖到小球上。
- 声音
阅读官方 Audio 手册
用博客给出游戏中利用 Reverb Zones 呈现车辆穿过隧道的声效的案例
创建一个空对象,往空对象中增加两个组件,分别是Audio source和Audio Reverb Zone。然后将Audio Reverb Zone中的Reverb Preset改成cave。再往Audio source中添加相应的音频即可。