3D游戏作业3-鼠标打飞碟
游戏内容要求:
-
游戏有 n 个 round,每个 round 都包括10 次 trial。
-
每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制。
-
每个 trial 的飞碟有随机性,总体难度随 round 上升。
-
鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
游戏的要求:
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类。
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离。
游戏演示及代码
代码 将hw5的assets拖到项目中,然后打开Resources中的myscene,运行即可。
游戏实现:
项目中的SSDirector与之前项目相似,这里不做赘述。
1.disk预制件
其中包含了三个属性,类型,分数和颜色,要得到三种不同的飞碟只需要把这个预制件拖到三个飞碟上,然后改变属性的值就可以得到三个预制实例化。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 飞碟
public class Disk : MonoBehaviour {
public int type;
public int score;
public Color color;
}
2.飞碟工厂DiskFactory
使用工厂方法使用者就能够在不需要知道对象如何实例化的情况下得到对象实例,而且可以减少游戏对象销毁频率,提升游戏的性能。该脚本维护了两个序列,useddisklist和freedisklist,分别存储已经被使用的飞碟和未被使用的飞碟。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 工厂
public class DiskFactory : MonoBehaviour {
private List<Disk> useddisklist = new List<Disk>();
private List<Disk> freedisklist = new List<Disk>();
public GameObject GetDisk(int type) {
GameObject disk_prefab = null;
//寻找空闲飞碟,如果无空闲飞碟则重新实例化飞碟
if (freedisklist.Count>0) {
for(int i = 0; i < freedisklist.Count; i++) {
if (freedisklist[i].type == type) {
disk_prefab = freedisklist[i].gameObject;
freedisklist.Remove(freedisklist[i]);
break;
}
}
}
if(disk_prefab == null) {
if(type == 1) {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk1"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
else if (type == 2) {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk2"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
else {
disk_prefab = Instantiate(
Resources.Load<GameObject>("Prefabs/disk3"),
new Vector3(0, -10f, 0), Quaternion.identity);
}
disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<Disk>().color;
}
useddisklist.Add(disk_prefab.GetComponent<Disk>());
disk_prefab.SetActive(true);
return disk_prefab;
}
//摧毁飞碟
public void FreeDisk() {
for(int i=0; i<useddisklist.Count; i++) {
if (useddisklist[i].gameObject.transform.position.y <= -10f) {
freedisklist.Add(useddisklist[i]);
useddisklist.Remove(useddisklist[i]);
}
}
}
public void Reset() {
FreeDisk();
}
}
3.记分员ScoreRecorder
负责统计分数并提供了一个返回分数的函数GetScore。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//记录分数
public class ScoreRecorder : MonoBehaviour {
private float score;
void Start () {
score = 0;
}
public void Record(GameObject disk) {
score += disk.GetComponent<Disk>().score;
}
public float GetScore() {
return score;
}
public void Reset() {
score = 0;
}
}
4.场景单实例Singleton
如果此前不存在该实例,就新创建一个实例并返回,否则就返回原有的实例,这样采用单实例的方式就可以避免重复创建游戏对象导致的资源浪费。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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;
}
}
}
5.接口interface
定义了几个接口,方便不同控制器或场景调用彼此的函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController {
void LoadResources();
}
public interface IUserAction {
void Hit(Vector3 pos);
float GetScore();
int GetRound();
int GetTrial();
void GameOver();
void ReStart();
bool Getflag();
float GetTarget();
bool GetStatus();
}
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);
}
6.SSAction
动作基类,表示该动作事物的共性,其中enable表示该动作是否进行,destory表示是否删除该动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//动作基类
public class SSAction : ScriptableObject {
public bool enable = true;
public bool destroy = false;
public GameObject gameobject;
public Transform transform;
public ISSActionCallback callback;
protected SSAction() { }
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
7.飞碟动作实现DiskFlyAction
继承了动作基类SSAction,表示飞碟的动作,具体是给飞碟一个初速度和为0的加速度,并给定重力加速度,通过计算得到飞碟每个时刻的位置,进而通过update函数更新飞碟的位置,进而控制飞碟的动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 模拟飞行
public class DiskFlyAction : SSAction {
public float gravity = -1; //向下的加速度
private Vector3 start_vector; //初速度向量
private Vector3 gravity_vector = Vector3.zero; //加速度的向量,初始时为0
private Vector3 current_angle = Vector3.zero; //当前时间的欧拉角
private float time; //已经过去的时间
private DiskFlyAction() { }
public static DiskFlyAction GetSSAction(int lor, float angle, float power) {
//初始化物体将要运动的初速度向量
DiskFlyAction action = CreateInstance<DiskFlyAction>();
if (lor == -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() {
//计算物体的向下的速度,v=at
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 < -10) {
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
8.SSActionManager
动作管理基类,表示动作管理的抽象。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/*动作管理基类*/
public class SSActionManager : MonoBehaviour, ISSActionCallback {
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();
//对于字典中每一个pair,看是执行还是删除
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);
Object.Destroy(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) {
}
}
9.FlyActionManger
管理飞碟的动作,通过调用DiskFlyAction管理飞碟的动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlyActionManager : SSActionManager {
public DiskFlyAction fly;
public FirstController myscenecontroller;
protected void Start() {
myscenecontroller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
myscenecontroller.myactionmanager = this;
}
//飞碟飞行
public void DiskFly(GameObject disk, float angle, float power) {
int lor = 1;
if (disk.transform.position.x > 0) lor = -1;
fly = DiskFlyAction.GetSSAction(lor, angle, power);
this.RunAction(disk, fly, this);
}
}
10.FirstController
管理游戏的进行。以下展示部分重要代码的实现。
游戏有三个回合,通过switch函数在不同的回合有不同的逻辑,下面以第一回合为例。
首先如果达到飞碟数达到10个,说明回合已经结束,通过控制暂停2秒让存留的飞碟飞出摄像机的视野范围,然后再暂停3秒,暂停的3秒钟里roundend为true,从而让UserGUI展示回合数以及目标分数。flag2方便进入一个新的round时只让timenow初始化一次。
if (count >= 100) {
//如果回合结束就等待5-2=3秒
if(roundend)
{
float tmp=System.DateTime.Now.Hour*3600+System.DateTime.Now.Minute*60+System.DateTime.Now.Second;
if(tmp<(timenow+5))
{
return;
}
else
{
num=0;
round=3;
//将flag重置,方便下个round使用
roundend=false;
flag2=false;
}
}
if (num == 10) {
//保存现在时间,并将flag2设置为true,下次update不会重置timenow的值。
if(flag2==false)
{
timenow=System.DateTime.Now.Hour*3600+System.DateTime.Now.Minute*60+System.DateTime.Now.Second;
flag2=true;
}
//等待两秒
else{
float tmp=System.DateTime.Now.Hour*3600+System.DateTime.Now.Minute*60+System.DateTime.Now.Second;
if(tmp<(timenow+2))
{
return;
}
//将flag重置,方便下个round使用
flag2=false;
checkstatus();
if(status==true)
{
roundend=true;
}
}
}
else
{
count = 0;
if (num % 2 == 0) SendDisk(1);
else SendDisk(2);
num += 1;
}
}
Hit函数检测鼠标点出的射线是否射到了飞碟,如果射到了就加分并将飞碟回收。
public void Hit(Vector3 pos) {
Ray ray = Camera.main.ScreenPointToRay(pos);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i++) {
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<Disk>() != null) {
myscorerecorder.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
}
}
}
SendDisk函数通过调用飞碟工厂的函数获得飞碟,然后对飞碟的属性进行初始化后发射。
private void SendDisk(int type) {
GameObject disk = mydiskfactory.GetDisk(type);
float ran_y = 0;
float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
float power = 0;
float angle = 0;
if (type == 1) {
ran_y = Random.Range(1f, 5f);
power = Random.Range(4f, 6f);
angle = Random.Range(25f,30f);
}
else if (type == 2) {
ran_y = Random.Range(2f, 3f);
power = Random.Range(5f, 7f);
angle = Random.Range(15f, 17f);
}
else {
ran_y = Random.Range(5f, 6f);
power = Random.Range(6f, 8f);
angle = Random.Range(10f, 12f);
}
disk.transform.position = new Vector3(ran_x*16f, ran_y, 0);
myactionmanager.DiskFly(disk, angle, power);
}
11.UserGUI
游戏界面
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour {
private IUserAction action;
//每个GUI的style
GUIStyle bold_style = new GUIStyle();
GUIStyle text_style = new GUIStyle();
GUIStyle text_style2 = new GUIStyle();
GUIStyle over_style = new GUIStyle();
GUIStyle round_style = new GUIStyle();
GUIStyle target_style = new GUIStyle();
private bool game_start = false;
void Start () {
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
}
void OnGUI () {
bold_style.normal.textColor = new Color(1, 0, 0);
bold_style.fontSize = 16;
text_style2.normal.textColor = new Color(240, 248, 255);
text_style2.fontSize = 16;
text_style.normal.textColor = new Color(0, 3, 181,190);
text_style.fontSize = 16;
over_style.normal.textColor = new Color(1, 0, 0);
over_style.fontSize = 25;
round_style.normal.textColor = new Color(1, 0, 0);
round_style.fontSize = 70;
target_style.normal.textColor = new Color(1, 0, 0);
target_style.fontSize = 30;
if (game_start) {
GUI.Label(new Rect(10, 5, 200, 50), "分数:"+ action.GetScore().ToString(), text_style);
GUI.Label(new Rect(80, 5, 50, 50), "Round:" + action.GetRound().ToString(), text_style);
GUI.Label(new Rect(150, 5, 50, 50), "trial:" + action.GetTrial().ToString(), text_style);
GUI.Label(new Rect(600, 5, 50, 50), "白色:1分", text_style);
GUI.Label(new Rect(600, 25, 50, 50), "黄色:2分", text_style);
GUI.Label(new Rect(600, 45, 50, 50), "橙色:3分", text_style);
if(action.Getflag())
{
GUI.Label(new Rect(Screen.width / 2 - 110, Screen.height / 2 - 60 , 100, 100), "Round"+(action.GetRound()+1).ToString(), round_style);
GUI.Label(new Rect(Screen.width / 2 - 120, Screen.height / 2 +20 , 100, 100), "本回合目标分数为"+(action.GetTarget()).ToString(), target_style);
}
if (action.GetStatus()==false) {
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 80, 100, 100), "游戏失败", over_style);
GUI.Label(new Rect(Screen.width / 2 - 45, Screen.height / 2 - 30, 50, 50), "你的分数:" + action.GetScore().ToString(), text_style2);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2+40, 100, 50), "重新开始")) {
action.ReStart();
return;
}
action.GameOver();
}
else if(action.GetStatus()==true&&action.Getflag())
{
GUI.Label(new Rect(Screen.width / 2 - 110, Screen.height / 2 - 60 , 100, 100), "Round"+(action.GetRound()+1).ToString(), round_style);
GUI.Label(new Rect(Screen.width / 2 - 120, Screen.height / 2 +20 , 100, 100), "本回合目标分数为"+(action.GetTarget()).ToString(), target_style);
}
if (action.GetRound() == 3 && action.GetTrial() == 11&&action.GetStatus()) {
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 80, 100, 100), "游戏成功", over_style);
GUI.Label(new Rect(Screen.width / 2 - 45, Screen.height / 2 - 30, 50, 50), "你的分数:" + action.GetScore().ToString(), text_style2);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2+40, 100, 50), "重新开始")) {
action.ReStart();
return;
}
action.GameOver();
}
}
else {
GUI.Label(new Rect(Screen.width / 2 - 60, Screen.height / 2 - 100, 100, 100), "简单打飞碟", over_style);
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 50, 100, 100), "鼠标点击飞碟", text_style2);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2, 100, 50), "游戏开始")) {
game_start = true;
action.ReStart();
}
}
}
}