3D游戏第五次作业
一、编写一个简单的鼠标打飞碟(Hit UFO)游戏
要求:
- 游戏有 n 个 round,每个 round 都包括10 次 trial;
- 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
- 每个 trial 的飞碟有随机性,总体难度随 round 上升;
- 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可自由设定。
- 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
- 近可能使用前面 MVC 结构实现人机交互与游戏模型分离
效果展示:
实现:
预制飞盘对象,使用MVC 结构的工厂模式,代码结构如下:
SSDirector类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object {
private static SSDirector _instance;
public ISceneController CurrentScenceController { get; set; }
public static SSDirector GetInstance() {
if (_instance == null) {
_instance = new SSDirector();
}
return _instance;
}
}
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();
}
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);
}
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;
}
}
}
Disk类:飞盘对象
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Disk : MonoBehaviour {
public int type = 1;
public int score = 1;
public Color color = Color.white;
}
DiskFactory类:工厂模式负责生产和回收飞碟
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// 工厂
public class DiskFactory : MonoBehaviour {
private List<Disk> used = new List<Disk>();
private List<Disk> free = new List<Disk>();
public GameObject GetDisk(int type) {
GameObject disk_prefab = null;
//寻找空闲飞碟,如果无空闲飞碟则重新实例化飞碟
if (free.Count>0) {
for(int i = 0; i < free.Count; i++) {
if (free[i].type == type) {
disk_prefab = free[i].gameObject;
free.Remove(free[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;
}
used.Add(disk_prefab.GetComponent<Disk>());
disk_prefab.SetActive(true);
return disk_prefab;
}
public void FreeDisk() {
for(int i=0; i<used.Count; i++) {
if (used[i].gameObject.transform.position.y <= -10f) {
free.Add(used[i]);
used.Remove(used[i]);
}
}
}
public void Reset() {
FreeDisk();
}
}
SSAction类:动作基类
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; //动作对象的transform
public ISSActionCallback callback; //回调函数
/*防止用户自己new对象*/
protected SSAction() { }
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
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>(); //等待删除动作的key的列表
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) {
}
}
SequenceAction类:组合动作类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence; //动作的列表
public int repeat = -1; //-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()
{
}
}
DiskFlyAction类:飞盘运动类
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;
//如果物体y坐标小于-1,动作就做完了
if (this.transform.position.y < -1) {
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
public override void Start() { }
}
FlyActionManager类:飞盘运动管理类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FlyActionManager : SSActionManager {
public DiskFlyAction fly;
public FirstController scene_controller;
protected void Start() {
scene_controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
scene_controller.action_manager = 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);
}
}
ScoreRecorder类:记录分数类
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;
}
}
FirstControllor类:总控制类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
public FlyActionManager action_manager;
public DiskFactory disk_factory;
public UserGUI user_gui;
public ScoreRecorder score_recorder;
private int round = 1;
private int trial = 0;
//private float speed = 1f;
private bool running = false;
void Start () {
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
disk_factory = Singleton<DiskFactory>.Instance;
score_recorder = Singleton<ScoreRecorder>.Instance;
action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
}
int count = 0;
//调整轮次,以及根据轮次修改发射规则
void Update () {
if(running) {
count++;
if (Input.GetButtonDown("Fire1")) {
Vector3 pos = Input.mousePosition;
Hit(pos);
}
switch (round) {
case 1: {
if (count >= 150) {
count = 0;
SendDisk(1);
trial += 1;
if (trial == 10) {
round += 1;
trial = 0;
}
}
break;
}
case 2: {
if (count >= 100) {
count = 0;
if (trial % 2 == 0) SendDisk(1);
else SendDisk(2);
trial += 1;
if (trial == 10) {
round += 1;
trial = 0;
}
}
break;
}
case 3: {
if (count >= 50) {
count = 0;
if (trial % 3 == 0) SendDisk(1);
else if(trial % 3 == 1) SendDisk(2);
else SendDisk(3);
trial += 1;
if (trial == 10) {
running = false;
}
}
break;
}
default:break;
}
disk_factory.FreeDisk();
}
}
public void LoadResources() {
disk_factory.GetDisk(round);
disk_factory.FreeDisk();
}
//发射飞碟
private void SendDisk(int type) {
//从工厂中拿一个飞碟
GameObject disk = disk_factory.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(5f, 20f);
angle = Random.Range(25f,30f);
}
else if (type == 2) {
ran_y = Random.Range(2f, 3f);
power = Random.Range(10f, 20f);
angle = Random.Range(15f, 17f);
}
else {
ran_y = Random.Range(5f, 6f);
power = Random.Range(15f, 20f);
angle = Random.Range(10f, 17f);
}
disk.transform.position = new Vector3(ran_x*16f, ran_y, 0);
action_manager.DiskFly(disk, angle, power);
}
//检测射线与飞碟是否碰撞,如碰撞则计分并回收飞碟
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) {
score_recorder.Record(hit.collider.gameObject);
hit.collider.gameObject.transform.position = new Vector3(0, -10, 0);
}
}
}
public float GetScore() {
return score_recorder.GetScore();
}
public int GetRound() {
return round;
}
public int GetTrial() {
return trial;
}
//重新开始
public void ReStart() {
running = true;
score_recorder.Reset();
disk_factory.Reset();
round = 1;
trial = 1;
//speed = 2f;
}
//游戏结束
public void GameOver() {
running = false;
}
}
GUI
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 over_style = new GUIStyle();
private bool game_start = false;
//控制规则显示
bool isShow = 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_style.normal.textColor = new Color(0, 0, 0, 1);
text_style.fontSize = 16;
over_style.normal.textColor = new Color(1, 0, 0);
over_style.fontSize = 25;
GUIStyle button_style = new GUIStyle("button")
{
fontSize = 15
};
if (GUI.Button(new Rect(10, 10, 60, 30), "Rule", button_style))
{
if (isShow)
isShow = false;
else
isShow = true;
}
if(isShow)
{
GUI.Label(new Rect(Screen.width / 2 - 400 , 70, 100, 50), "点击Start开始游戏", text_style);
GUI.Label(new Rect(Screen.width / 2 - 400, 90, 250, 50), "游戏过程中鼠标左键为hit", text_style);
GUI.Label(new Rect(Screen.width / 2 - 400, 110, 250, 50), "随着时间的推移,难度会上升", text_style);
GUI.Label(new Rect(Screen.width / 2 - 400, 130, 250, 50), "上吧!!!", text_style);
}
if (game_start) {
GUI.Label(new Rect(Screen.width - 150, 5, 200, 50), "Score:"+ action.GetScore().ToString(), text_style);
GUI.Label(new Rect(100, 5, 50, 50), "Round:" + action.GetRound().ToString(), text_style);
GUI.Label(new Rect(180, 5, 50, 50), "Trial:" + action.GetTrial().ToString(), text_style);
if (action.GetRound() == 3 && action.GetTrial() == 10) {
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 100, 100, 100), "GAME OVER", over_style);
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 50, 50, 50), "YOUR SCORE: " + action.GetScore().ToString(), over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 20, Screen.height / 2, 100, 50), "RESTART")) {
action.ReStart();
return;
}
action.GameOver();
}
}
else {
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.height / 2 - 100, 100, 100), "Hit UFO", over_style);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.height / 2, 100, 50), "START")) {
game_start = true;
action.ReStart();
}
}
}
}
二、编写一个简单的自定义 Component
DiskEditor类:序列化飞碟属性,如颜色,大小,分数并添加自定义属性
using UnityEngine;
using UnityEditor;
using System.Collections;
[CustomEditor(typeof(Disk))]
[CanEditMultipleObjects]
public class DiskEditor : Editor
{
SerializedProperty score;
SerializedProperty color;
SerializedProperty scale;
void OnEnable()
{
score = serializedObject.FindProperty("score");
color = serializedObject.FindProperty("color");
scale = serializedObject.FindProperty("scale");
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score"));
serializedObject.ApplyModifiedProperties();
}
}