游戏对象与图形基础
基本操作演练
下载Fantasy Skybox FREE,构建自己的游戏场景
首先在window中找到Assert Store:
在Asset Store中搜索Fantasy Skybox Free,并选择第一个素材:
下载之后将素材import到你的游戏中:
导入素材之后可以看到工程路径多出了以下文件夹:
然后添加一个Skybox:
随便选择一个素材中的天空盒场景拖入到刚刚创建的Skybox中:
最后就可以看到天空盒场景了:
然后我们在游戏场景中添加一个terrain地形,并且随便添加几个Fantasy Skybox中的素材,然后将它们添加到Tree中:
将添加的树和石头之类的素材加入到场景中,就可以得到以下效果:
可以看到上图的地面还是白色的,这时我们需要给地形添加新的材质:
修改材质之后得到以下效果:
从上面的实践过程可以感受到,我们可以很容易的从asset store中获得很多优质的素材。有了这些素材和unity方便的地形工具,我们可以很快构建出我们需要的游戏场景。
编程实践
牧师与恶魔 动作分离版
在上次的牧师与恶魔游戏中,可以很明显的感觉到我们的控制器管理了太多的东西,控制器管理了游戏场景的加载、管理游戏规则、管理游戏对象的运动等等。所以在这一部分中,我们需要将管理游戏对象运动的功能分离出来,专门实现一个动作管理器。
动作管理器的设计基本上是遵循老师课件中提供的内容,实现的内容有:
功能 | 类名 |
动作基类 | SSAction |
简单动作实现 | CCMoveToAction |
动作事件接口 | ISSActionCallback |
动作管理基类 | SSActionManager |
动作管理实现 | CCActionManager |
SSAction
SSAction中定义了动作基类,所有动作实例都必须继承这个类。在SSAction中声明了动作应用的游戏对象,动作完成后的回调函数,并且通过virtual虚函数定义声明了子类必须实现的函数Start和Update函数。
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destory = false;
public GameObject gameobject { get; set; }
public Transform transform { get; set; }
public ISSActionCallback callback { get; set; }
protected SSAction() { }
// Start is called before the first frame update
public virtual void Start()
{
throw new System.NotImplementedException();
}
// Update is called once per frame
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
CCMoveToAction
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)
{
this.destory = true;
this.callback.SSActionEvent(this);
}
}
public override void Start()
{
}
}
ISSActionCallback
ISSActionCallback定义了动作事件的接口。C#中接口的使用我们在上一次的作业中已经涉及了。继承了ISSActionCallback接口的类必须实现SSActionEvent函数,也就是之后的动作管理器必须实现这个函数。
public enum SSActionEventType : int { Started, Competeted}
public interface ISSActionCallback
{
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0,
string strParam = null,
GameObject objectParam = null);
}
SSActionManager
SSActionManager是动作管理的基类,实现了对所有动作的基本管理。可以看到其中waitingAdd 保存了所有等待添加的动作,这是由于每一帧的间隔时间内可能也有新的动作需要之后执行,所以需要一个等待队列。actions保存了每一帧需要执行的动作。在update函数中,首先将waitingAdd等待列表中的动作全部加到actions中,然后依次执行actions中的所有动作。
当我们需要添加一个动作时,我们可以调用RunAction函数将动作对应的游戏对象、动作对应的动作实现SSAction、以及动作完成时的回调函数。
public class SSActionManager : MonoBehaviour
{
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();
protected void Update()
{
foreach (SSAction ac in waitingAdd)
{
actions[ac.GetInstanceID()] = ac;
}
waitingAdd.Clear();
foreach (KeyValuePair<int, SSAction> kv in actions)
{
SSAction ac = kv.Value;
if (ac.destory)
{
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();
}
void Start() {
}
}
CCActionManager
CCActionManager是继承了动作管理基类的一个动作管理器实例。CCActionManager中重写了start函数,并且通过SSDirector获得了当前的场景控制器,同时将当前场景控制器中的actionmanager设置为自己。
场景控制器可以通过调用CCActionManager中的Move函数,并以需要移动的游戏对象、目标位置、移动速度为参数,Move函数中使用了CCMoveToAction动作实例。
在SSActionEvent中实现了ISSActionCallback要求的接口函数,在这里定义了当动作完成后需要运行的回调函数。这个动作管理器只负责移动船以及船上的乘客,在每次移动完成后我们需要检查当前游戏状态。所以这些需要在运动完成后执行的操作都写在这个回调函数中。
public class CCActionManager: SSActionManager, ISSActionCallback
{
public Controller sceneController;
public CCMoveToAction move_action;
protected new void Start()
{
Debug.Log("action start");
sceneController = SSDirector.getInstance().currentSceneController as Controller;
sceneController.actionManager = this;
}
public void Move(GameObject thing, Vector3 target, float speed)
{
move_action = CCMoveToAction.GetSSAction(target, speed);
this.RunAction(thing, move_action, this);
}
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0,
string strParam = null,
GameObject objectParam = null)
{
sceneController.enable_all();
sceneController.change_game_situation();
Debug.Log("move finish");
}
}
完成了动作管理器之后,我们就要开始对控制器进行一些修改。在场记(Controller)中添加一个CCActionManager的实例,并在Awake函数中创建它。
private GameObject left_land, right_land, river;
private CharacterModel[] MCharacter;
private BoatModel MBoat;
public CCActionManager actionManager;
// Start is called before the first frame update
void Awake()
{
SSDirector director = SSDirector.getInstance();
director.currentSceneController = this;
MCharacter = new CharacterModel[6];
director.currentSceneController.LoadResources();
actionManager = gameObject.AddComponent<CCActionManager>() as CCActionManager;
}
在场记的move_boat方法中实现同时移动船以及船上的乘客。在上次的代码中move_boat是通过调用船和角色的控制器中的方法来实现移动游戏对象,现在我们通过CCActionManager中的move方法实现游戏对象的移动。我们只需要通过控制器获得游戏对象的目标移动位置,并将游戏对象作为参数传递给move方法,然后就交给动作管理器来控制游戏对象的移动了。
为了保证物体运动的过程中玩家不能对游戏中的物体进行点击操作,在上次的实现中我设置了一个boat_moving标志位,当物体在移动的过程中boat_moving设置为true,当物体到达时设为false,并且在update中检查物体是否到达。有了运动管理器我们就不需要使用这么麻烦的方式检查物体是否还在移动了,我们只需要在运动开始时禁止用户操作,并且在运动结束的回调函数中使能用户操作。
public void move_boat()
{
Debug.Log("Move boat");
if (MBoat.is_empty())
return;
MBoat.turn_side();
int[] custom_num = MBoat.get_customs();
if (custom_num[0] != -1)
{
MCharacter[custom_num[0]].turn_side();
actionManager.Move(MCharacter[custom_num[0]].character, MCharacter[custom_num[0]].get_dst(), 20);
}
if (custom_num[1] != -1)
{
MCharacter[custom_num[1]].turn_side();
actionManager.Move(MCharacter[custom_num[1]].character, MCharacter[custom_num[1]].get_dst(), 20);
}
actionManager.Move(MBoat.boat, MBoat.get_dst(), 20);
this.stop_all();
Debug.Log(UserGUI.situation);
}
裁判类实现
在上一部分中,我们将动作管理的功能从场景控制器中分离了出来。在这一部分中,我们需要将判断游戏状态的功能从场景控制器中分离,形成一个专门用于检测游戏状态的裁判类,当游戏结束时通知场景控制器。
首先我们使用一个枚举类型定义当前的游戏状态:
public enum SituationType : int { Continue, Win, Loss }
然后再定义一个裁判类必须实现的一个函数接口:
public interface IGetSituation
{
SituationType GetSituation();
}
CCJudgement
最后在CCJudgement中实现GetSituation函数,在GetSituation函数中裁判类通过Controller的接口获得当前场景中的信息,并根据场上信息判断当前游戏状态。在场记中,只需要调用裁判类的接口就可以获得游戏状态并根据游戏状态进行进一步操作。
public class CCJudgement : MonoBehaviour, IGetSituation
{
public Controller sceneController;
// Start is called before the first frame update
void Start()
{
sceneController = SSDirector.getInstance().currentSceneController as Controller;
sceneController.judgement = this;
}
public SituationType GetSituation()
{
int left_d = 0, left_p = 0, right_d = 0, right_p = 0;
for (int i = 0; i < 6; i++)
{
if (i < 3)
{
if (sceneController.get_character_side(i) == -1)
right_d += 1;
else
left_d += 1;
}
else
{
if (sceneController.get_character_side(i) == -1)
right_p += 1;
else
left_p += 1;
}
}
if (left_d == 3 && left_p == 3) return SituationType.Win;
if (((left_d > left_p) && (left_p != 0)) || ((right_d > right_p) && (right_p != 0))) return SituationType.Loss;
return SituationType.Continue;
}
}
现在我们实现了一个简单的游戏裁判类来判断当前的游戏状态。有了裁判类,我们可以很容易的更改游戏规则,而场景控制器则不需要了解当前游戏状态是如何判断出来的,只需要使用裁判类的接口就好了,这使得各个类的分工更加明确。
游戏运行效果:
结合在第一部分实现的地形背景,以及之后实现的动作管理器和裁判类,游戏运行效果如下: