与游戏世界交互
编写一个简单的鼠标打飞碟游戏
游戏内容要求:
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 尽可能使用前面 MVC 结构实现人机交互与游戏模型分离
飞碟额外属性 UfoProperty
在这个游戏中,我们用胶囊体来作为飞碟,然后做一个统一的预制ufo.prefab。
虽然我们可以用一个统一的预制来被所有飞碟共用,但是这个游戏要求飞碟要有不同的颜色、不同的大小,而且击中之后有不同的得分。由于飞碟的颜色和大小可以在创建时就设定,所以不需要额外保存这些属性。而飞碟的种类、飞碟被击中时的分数还有飞碟的初始位置,我们需要用一个额外的类UfoProperty 保存这些属性。这些属性用于之后在厂模式中区分不同的飞碟,以及在飞碟被击中后的计分。
public class UfoProperty : MonoBehaviour
{
public int ufo_kind = 0;
public int score = 1;
public Vector3 direction;
}
游戏裁判类
将游戏中的规则管理相关的功能提取出来放到CCJudgement裁判类中实现。在这个游戏中需要管理的游戏规则相关的内容就是玩家的生命值、当前的分数,以及如何判断玩家当前的游戏状态和当前的难度。CCJudgement中给场景控制器提供接口,使得场景控制器可以直接查询当前的游戏状态和游戏难度,而不需要知道详细的规则是什么样的。
public enum SituationType : int { Continue, Loss }
public class CCJudgement : MonoBehaviour
{
public int life;
public int score;
private int round2 = 10;
private int round3 = 20;
void Start ()
{
life = 5;
score = 0;
}
public SituationType GetSituation()
{
if (life <= 0) return SituationType.Loss;
return SituationType.Continue;
}
public void Record(GameObject disk)
{
score += disk.GetComponent<DiskData>().score;
}
public void Reset()
{
life = 5;
score = 0;
}
public int Get_round()
{
if (score < round2) return 1;
else if (score < round3) return 2;
else return 3;
}
public void Miss()
{
life -= 1;
}
}
用户界面
我们依然沿用上次作业构建的框架,由UserGUI来管理与用户交互相关的内容。在这个游戏中用户交互内容也非常简单,用户只有点击飞碟和重开游戏两个操作,这两个操作接口在IUserAction中给出。UserGUI从场景控制器中获取当前玩家的血量、分数以及游戏难度等级,并将这些信息显示在游戏界面上。
public class UserGUI : MonoBehaviour
{
private IUserAction action;
void Start ()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
void OnGUI ()
{
GUISkin skin = GUI.skin;
skin.button.normal.textColor = Color.black;
skin.label.normal.textColor = Color.black;
skin.button.fontSize = 20;
skin.label.fontSize = 20;
GUI.skin = skin;
int life = action.GetLife();
if (Input.GetButtonDown("Fire1"))
action.Hit(Input.mousePosition);
if(life > 0)
{
string life_string = "";
for (int i = 0; i < life; i++) life_string += "#";
GUI.Label(new Rect(0, 0, Screen.width / 8, Screen.height / 16), "Life:" + life_string);
GUI.Label(new Rect(0, Screen.height / 16, Screen.width / 8, Screen.height / 16), "Score:" + action.GetScore().ToString());
GUI.Label(new Rect(0, Screen.height / 8, Screen.width / 8, Screen.height / 16), "Round:" + action.GetRound().ToString());
}
else
{
GUI.Label(new Rect(Screen.width/2-60, Screen.height*5/16, 120, Screen.height / 8), "Game Over!");
GUI.Label(new Rect(Screen.width/2-75, Screen.height*7/16, 150, Screen.height / 8), "Your score is:"+ action.GetScore().ToString());
if (GUI.Button(new Rect(Screen.width * 7 / 16, Screen.height * 9 / 16, Screen.width / 8, Screen.height / 8), "restart"))
{
action.ReStart();
return;
}
}
}
}
动作控制器
遵循将各个功能分离的原则,我们还是和之前的游戏一样,将管理游戏对象运动的功能分离出来用专门的动作控制器管理。SSAction和SSActionmanager基类和上一次游戏中的完全一样,所以在这里就不再展示了。
FlyAction
我们只需要设置飞碟的起始位置、飞行角度和飞行初速度,我们就可以计算出飞碟的飞行轨迹。这个飞行动作和之前的MoveToAction唯一的区别就是运动轨迹的计算方式。
public class FlyAction : SSAction
{
public float gravity = -5;
private Vector3 start_vector;
private Vector3 gravity_vector = Vector3.zero;
private float time;
private Vector3 current_angle = Vector3.zero;
private FlyAction() { }
public static FlyAction GetSSAction(Vector3 direction, float angle, float power)
{
FlyAction action = CreateInstance<FlyAction>();
if (direction.x == -1)
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
}
else
{
action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
}
return action;
}
public override void Update()
{
time += Time.fixedDeltaTime;
gravity_vector.y = gravity * time;
transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
transform.eulerAngles = current_angle;
if (this.transform.position.y < 0)
{
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
CCActionManager
CCActionManager继承自SSActionManager基类,和上次的动作管理器没有什么区别。场景控制器可以通过调用Fly方法启用飞行动作。
public class CCActionManager : SSActionManager
{
public FlyAction fly;
public FirstController scene_controller;
protected void Start()
{
scene_controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
scene_controller.action_manager = this;
}
public void Fly(GameObject disk, float angle, float power)
{
fly = FlyAction.GetSSAction(disk.GetComponent<UfoProperty>().direction, angle, power);
this.RunAction(disk, fly, this);
}
}
工厂模式管理飞碟的生产与回收
如果我没有去了解一下工厂模式的话,管理飞碟我可能会很自然地使用Instantiate和Destory。当我们需要发射一个飞碟时就实例化一个飞碟,当一个飞碟飞到视野外或者落到地上后就销毁这个飞碟,但是这种游戏对象反复的实例化和销毁会严重的影响性能,为了更高效的对飞碟进行管理我们引入了工程模式。
工厂模式的核心思想是,我们创建一个游戏对象池,游戏对象池中有一些已经提前创建好的游戏对象。当我们需要发射一个飞碟时,我们就从对象池中取出一个对象并为其初始化一些属性,比如发射起始位置和方向等等。当一个飞碟需要被销毁时,我们并不是将他Destory掉,而是将它重新收回对象池并且将其隐藏。这样整个过程中都不涉及游戏对象的Instantiate和Destory。只有当我们需要使用一个游戏对象而对象池里却没有的时候我们才会进行一次Instantiate,并扩充对象池,Destory就完全不会调用。
Factory类
在这个类中我
创建时将当前的难度作为参数,Produce函数中按照不同难度下各种飞碟的比例来随机创建飞碟。根据工厂模式的规则,我们首先到对象池中去查找是否有我们需要的飞碟(根据之前我们创建的ufo_kind属性),如果对象池中有就直接返回,如果对象池中没有就重新创建。当我们需要回收飞碟的时候,就将对应飞碟添加到对象池中。
public class Factory : MonoBehaviour
{
private List<UfoProperty> used = new List<UfoProperty>();
private List<UfoProperty> free = new List<UfoProperty>();
public GameObject Produce(int round)
{
GameObject ufo_object = null;
float y = -10f;
float[][] property = new float[3][]; //每个round中不同种类飞碟的比例
property[0] = new float[3] { 0.7f, 0.9f, 1f };
property[1] = new float[3] { 0.5f, 0.8f, 1f };
property[2] = new float[3] { 0.3f, 0.7f, 1f };
int temp_result = Random.Range(0, 100);
float choice_result = (float)temp_result * 0.01f;
int ufo_kind = 0;
for (int i = 0; i < 3; i++)
{
if (choice_result <= property[round - 1][i]) {
ufo_kind = i;
break;
}
}
for (int i=0;i<free.Count;i++)
{
if (free[i].gameObject.GetComponent<UfoProperty>().ufo_kind == ufo_kind)
{
ufo_object = free[i].gameObject;
free.Remove(free[i]);
break;
}
}
if (ufo_object == null)
{
ufo_object = Instantiate(Resources.Load<GameObject>("Prefabs/ufo"), new Vector3(0, y, 0), Quaternion.identity);
ufo_object.GetComponent<UfoProperty>().ufo_kind = ufo_kind;
float x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
ufo_object.GetComponent<UfoProperty>().score = ufo_kind + 1;
ufo_object.GetComponent<UfoProperty>().direction = new Vector3(x, y, 0);
if (ufo_kind == 0)
{
ufo_object.GetComponent<Renderer>().material.color = Color.blue;
ufo_object.transform.localScale = new Vector3(2f, 0.25f, 2f);
}
else if (ufo_kind == 1)
{
ufo_object.GetComponent<Renderer>().material.color = Color.yellow;
ufo_object.transform.localScale = new Vector3(1.5f, 0.25f, 1.5f);
}
else
{
ufo_object.GetComponent<Renderer>().material.color = Color.red;
ufo_object.transform.localScale = new Vector3(1f, 0.25f, 1f);
}
}
used.Add(ufo_object.GetComponent<UfoProperty>());
return ufo_object;
}
public void Recover(GameObject disk)
{
for(int i = 0;i < used.Count; i++)
{
if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID())
{
used[i].gameObject.SetActive(false);
free.Add(used[i]);
used.Remove(used[i]);
break;
}
}
}
}
场景单实例
按照作业要求,工厂类Factory必须是场景单实例的。场景单实例我们使用模板类,并且继承MonoBehaviour,具体实现方法如下:(参考课程课件)
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;
public static T Instance
{
get
{
if (instance == null)
{
instance = (T)FindObjectOfType(typeof(T));
if (instance == null)
{
Debug.LogError("An instance of " + typeof(T)
+ " is needed in the scene, but there is none.");
}
}
return instance;
}
}
}
我们只需要在之后创建Factory实例的时候使用Singleton< Factory >.Instance来获取当前的工厂对象就可以了。
场景控制器
这部分的实现方式我学习了以前师兄的代码,特别是控制飞碟定时发射的部分。我们使用IvoleRepeating(“LoadResources”, start_time, period),来周期性调用LoadResources函数。但是当游戏难度提升时,我们需要先调用cancelIvoke函数中断当前的定时调用,然后修改调用频率之后再次创建一个新的定时调用,这样就可以实现随着游戏难度提升飞碟的发射频率也提升。
这里使用两个队列保存场上飞碟信息,一个disk_queue保存当前等待发射的飞碟,disk_notshot保存发射出去但是没有被集中的飞碟。当一个飞碟通过LoadResources创建之后,首先加入到disk_queue中。当调用send_disk函数发射飞碟时,才从disk_queue中取出一个飞碟,并将其放入disk_notshoo队列中。
当用户点击鼠标时,会通过UserGUI调用控制器的Hit函数,传入的参数是鼠标点击的位置。然后再Hit函数中会生成世界坐标的射线去击打飞碟,打中的飞碟会从disk_notshoot中删除,然后被工厂回收,并且将飞碟的ufodata属性传递给裁判类进行计分。
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public CCActionManager action_manager;
public Factory disk_factory;
public UserGUI user_gui;
public CCJudgement judgement;
private Queue<GameObject> disk_queue = new Queue<GameObject>();
private List<GameObject> disk_notshot = new List<GameObject>();
private int curr_round = 1;
private float speed = 2f;
private bool playing_game = false;
void Start ()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
disk_factory = Singleton<Factory>.Instance;
judgement = Singleton<CCJudgement>.Instance;
action_manager = gameObject.AddComponent<CCActionManager>() as CCActionManager;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
}
void Update ()
{
if (judgement.GetSituation()== SituationType.Loss)
{
CancelInvoke("LoadResources");
}
if (!playing_game)
{
InvokeRepeating("LoadResources", 1f, speed);
playing_game = true;
}
SendDisk();
if (judgement.Get_round() != curr_round)
{
curr_round = judgement.Get_round();
speed = 2 - 0.5f * curr_round;
CancelInvoke("LoadResources");
playing_game = false;
}
}
public void LoadResources()
{
disk_queue.Enqueue(disk_factory.Produce(judgement.Get_round()));
}
private void SendDisk()
{
float position_x = 16;
if (disk_queue.Count != 0)
{
GameObject disk = disk_queue.Dequeue();
disk_notshot.Add(disk);
disk.SetActive(true);
float ran_y = Random.Range(1f, 4f);
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
disk.GetComponent<UfoProperty>().direction = new Vector3(ran_x, ran_y, 0);
Vector3 position = new Vector3(-disk.GetComponent<UfoProperty>().direction.x * position_x, ran_y, 0);
disk.transform.position = position;
float power = Random.Range(10f, 15f);
float angle = Random.Range(15f, 28f);
action_manager.Fly(disk,angle,power);
}
for (int i = 0; i < disk_notshot.Count; i++)
{
GameObject temp = disk_notshot[i];
if (temp.transform.position.y < 0 && temp.gameObject.activeSelf == true)
{
disk_factory.Recover(disk_notshot[i]);
disk_notshot.Remove(disk_notshot[i]);
judgement.Miss();
}
}
}
public void Hit(Vector3 pos)
{
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
bool not_hit = false;
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<UfoProperty>() != null)
{
for (int j = 0; j < disk_notshot.Count; j++)
{
if (hit.collider.gameObject.GetInstanceID() == disk_notshot[j].gameObject.GetInstanceID())
{
not_hit = true;
}
}
if(!not_hit)
{
return;
}
disk_notshot.Remove(hit.collider.gameObject);
judgement.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -9, 0);
disk_factory.Recover(hit.collider.gameObject);
break;
}
}
}
public int GetScore()
{
return judgement.score;
}
public int GetLife()
{
return judgement.life;
}
public int GetRound()
{
return curr_round;
}
public void ReStart()
{
playing_game = false;
judgement.Reset();
curr_round = 1;
speed = 2f;
}
}
游戏运行效果
在这次游戏开发过程中主要的收获就是学会使用工厂模式管理游戏对象,以及学会了更多用户与游戏交互的方式,同时还复习了在前几次作业中用到的MVC结构。详细工程代码参见我的github,如果有什么问题请各位读者及时指出,谢谢。
转载:https://blog.csdn.net/Eric3778/article/details/102420495