1.游戏简介
在美国,有一种游戏叫飞碟射击(clay pigeon shooting),又称为打飞碟。它是一种用枪械射击抛在空中作为飞靶的圆形黏土碟的射击运动。英文中对飞碟的称呼是“黏土鸽子”(clay pigeon)或“鸟”(bird),原因是过去英国的射击比赛曾使用活鸽子作为靶子,虽然在1921年被立法禁止而改用陶碟模拟飞鸟,但叫法还是保留了下来,把命中目标叫“打死”(kill)、没能击中叫“鸟逃了”(bird away)、发射飞碟的机械叫“鸟阱”(trap)。
我们用游戏模拟现实世界,本次游戏就模拟现实世界中的飞碟射击运动,并将这一运动进行简化,我们不再利用枪支打飞碟,而是利用鼠标我们就可以进行飞碟射击运动,鼠标点击飞碟可使飞碟消失,模拟真实世界中子弹击中飞碟的效果,现在点击开始游戏按钮并进行游玩吧。
2.游戏规则
1)玩家打到红色飞碟得1分,打到绿色飞碟得2分,打到蓝色飞碟得3分 。
2)一共发射五轮飞碟,对于第n轮,会发射2的n-1次方个飞碟,例如第一轮发射1个飞碟,第二轮发射2个飞碟,第三轮发射4个飞碟,以此类推,当所有飞碟发射完毕时游戏结束。
3.游戏玩法
使用鼠标点击飞镖,点击不同颜色的飞碟就有相应的得分,为了得到更高的分数,玩家需要在有限的轮数内尽力打到最多的飞碟,此外,轮次数越大,发射的飞碟数更多,玩家一次性无法打中所有飞碟,故玩家要选择得分高的颜色的飞碟打,这无疑增加了游戏的可玩性。
4.游戏UML图
MainControllor为主控制器,用于加载资源、开始游戏、重启游戏、判断游戏是否结束等。
IUserAction:用户动作基类接口,内含有用户能通过GUI进行的操作。
UserGUI:用户控制相关的GUI等。
CCFlyAction:主要用于回调,当飞碟飞出屏幕时进行回调。
ISSActionCallbck:实现回调函数。
SceneControllor:用于控制场景。
SSAction:动作基类接口。
SSActionManger:动作管理者基类并不直接作为动作管理者使用,它的存在是为调用者提供较简单和通用的接口。
SSDirector:场记。
DiskFactory:以工厂模式控制飞碟。
DiskData:飞碟基础数据。
CCActionManager:具体动作作为类中的一个对象从而管理动作。
5.游戏编写
本游戏基于Unity引擎编写,使用的系统为Windows10,使用的脚本文件语言为C#,首先需要确保保证电脑中安装Unity引擎,我的版本是2023.3.8f1c1,还需下载VSCode或VStudio用于编写代码,还需要Unity的Plastic SCM用于管理代码,具体安装方法这里不过多介绍。
接着我们打开Unity,找到项目,选择新项目。
选择3D,项目命名为clay pigeon shooting,保存地址我选择的是D盘。(启动版本管理可选可不选)。
现在我们进入了Unity引擎界面,如图所示,什么内容也没有。
本项目中运用到的代码、文件非常多,为了保证界面的简洁性,建议在Assests项目栏中如下图建
立几个文件夹方便管理。
建立方法如下:在Assets栏中空白区用鼠标右键点击,选择Create中的Folder生成我们需要的游戏文件。
然后我们先制作飞碟,先在Resources文件夹中建立文件夹Prefabs。
然后在文件夹Prefabs制作飞碟,在左侧SampleScene中右键鼠标,选择3D Object->Cylinder
将Cylinder命名为Disk_Prefab,我的飞碟各个参数设置如下,也可以自行设置飞碟。
然后在Scripts文件夹中编写代码,共有以下几个脚本:
我们先编写UserGUI脚本,UerGUI主要是设置Label和button,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
private IUserAction userAction;
public string gameMessage;
public int points;
bool gameStart = false; //判断是否在游戏中
void Start()
{
points = 0;
gameMessage = "";
userAction = SSDirector.GetInstance().CurrentScenceController as IUserAction;
gameStart = false;
}
void OnGUI()
{
//小字体初始化
GUIStyle style = new GUIStyle();
style.normal.textColor = Color.white;
style.fontSize = 30;
//大字体初始化
GUIStyle bigStyle = new GUIStyle();
bigStyle.normal.textColor = Color.white;
bigStyle.fontSize = 50;
if (!gameStart)
{
GUI.Label(new Rect(Screen.width * 0.5f - 195, Screen.height * 0.25f, 100, 100), "Clay Pigeon Shooting", bigStyle);
if (GUI.Button(new Rect(Screen.width * 0.5f - 50, Screen.height * 0.5f, 100, 40), "开始游戏"))
{
gameStart = true;
userAction.GameStart(gameStart);
}
}
if(gameStart)
{
GUI.Label(new Rect(Screen.width * 0.5f - 50, 20, 100, 30), "得分: " + points, style);
GUI.Label(new Rect(Screen.width * 0.5f - 50, Screen.height * 0.5f - 200, 100, 100), gameMessage, style);
userAction.SetMode(false);
if (GUI.Button(new Rect(20, 50, 100, 40), "Restart"))
{
userAction.Restart();
}
if (Input.GetButtonDown("Fire1"))
{
userAction.Hit(Input.mousePosition);
}
}
}
}
我们现在编写IUserAction脚本,用户有三个动作,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IUserAction
{
void Hit(Vector3 position);
void Restart();
void GameStart(bool Mystart);
}
我们现在编写DiskData,飞碟主要有speed、points和direction三个属性,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskData : MonoBehaviour
{
public float speed; //水平速度
public int points; //得分
public Vector3 direction; //初始方向
}
我们现在编写DiskFactory脚本,该脚本主要用于生成飞碟与释放飞碟,其中GetDisk用于生产飞碟,我们先从free列表中查找是否有空闲的飞碟,如果没有则另外创建一个新飞碟。为了确保难度是由简单到复杂,及低轮次数只产生分数比较小的飞碟(红色),高轮次数产生分数比较大的飞碟(蓝色与绿色),我们给每个飞碟设置等级,飞碟的等级与轮次数有关,我们在0~2之间的随机数乘以轮次数获得飞碟的等级,并按照等级设置飞碟的分数、颜色、速度、方向等基本信息,这样就实现了我们的目的。而FreeDisk用于释放飞碟,将飞碟从used列表中移除并添加到free列表中以供下一轮的使用。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class DiskFactory : MonoBehaviour
{
public GameObject disk_Prefab; //飞碟预制
private List<DiskData> used; //正被使用的飞碟
private List<DiskData> free; //空闲的飞碟
public void Start()
{
used = new List<DiskData>();
free = new List<DiskData>();
disk_Prefab = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/Disk_Prefab"), Vector3.zero, Quaternion.identity);
disk_Prefab.SetActive(false);
}
public GameObject GetDisk(int round)
{
GameObject disk;
//如果有空闲的飞碟,则直接使用,否则生成一个新的
if (free.Count > 0)
{
disk = free[0].gameObject;
free.Remove(free[0]);
}
else
{
disk = GameObject.Instantiate<GameObject>(disk_Prefab, Vector3.zero, Quaternion.identity);
disk.AddComponent<DiskData>();
}
//按照round来设置飞碟属性
//不同等级数对应不同颜色的飞镖,当等级数为:
//0~4是红色飞碟
//4~7是绿色飞碟
//7~10是蓝色飞碟
//飞碟的等级 = 0~2之间的随机数 * 轮次数
float level = UnityEngine.Random.Range(0, 2f) * (round + 1);
if (level < 4)//红色飞碟
{
disk.GetComponent<DiskData>().points = 1;
disk.GetComponent<DiskData>().speed = 4.0f;
disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<Renderer>().material.color = Color.red;
}
else if (level > 7)//绿色飞碟
{
disk.GetComponent<DiskData>().points = 3;
disk.GetComponent<DiskData>().speed = 8.0f;
disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<Renderer>().material.color = Color.blue;
}
else//蓝色飞碟
{
disk.GetComponent<DiskData>().points = 2;
disk.GetComponent<DiskData>().speed = 6.0f;
disk.GetComponent<DiskData>().direction = new Vector3(UnityEngine.Random.Range(-1f, 1f) > 0 ? 2 : -2, 1, 0);
disk.GetComponent<Renderer>().material.color = Color.green;
}
used.Add(disk.GetComponent<DiskData>());
return disk;
}
public void FreeDisk(GameObject disk)
{
//找到使用中的飞碟,将其踢出并加入到空闲队列
foreach (DiskData diskData in used)
{
if (diskData.gameObject.GetInstanceID() == disk.GetInstanceID())
{
disk.SetActive(false);
free.Add(diskData);
used.Remove(diskData);
break;
}
}
}
}
我们现在编写Singleton脚本,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;
}
}
}
我们现在编写SSAction脚本,在动作分离版本中,将动作其抽离为一个 SSAction 类,当这个物体需要进行某些动作时,再实时进行动作的创建,并进行Update()。并且为其定义一个 destroy 属性,当动作完成后自动将这个动作实例标记为可以删除的对象,等待游戏引擎自动回收这个对象,减少对系统资源的占用。具体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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()
{
}
// 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();
}
}
我们现在编写ISSActionCallBack脚本,当一个动作完成之后,我们需要通知其他的部分(如动作管理器或其他的动作)当前的动作已经完成,可以进行下一步的操作。因此添加一个SSActionCallbackInterface 类,使得在动作完成之后再进行一个回调操作,实现通知其他部分的这个功能。具体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
//回调函数
void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null);
}
我们现在编写CCFlyAction脚本,CCMoveToAction 继承于 SSAction,并给动作管理器提供了GetSSAction的接口。当动作管理器调用这个方法的时候,可以直接生成一个动作的脚本,此时只需要将这个脚本赋予给某个物体,并由动作管理器进行动作的调度之后,即可产生物体做出这个动作的效果。在CCFlyAction脚本中,我们将飞碟的抛物线运动分解为水平方向运动与垂直方向运动,水平速度恒定,垂直方向施加重力加速度。此外,当飞碟到达底部时,动作结束,将进行回调。具体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCFlyAction : SSAction
{
float gravity; //重力加速度
float speed; //水平速度
Vector3 direction; //飞行方向
float time; //时间
//生产函数(工厂模式)
public static CCFlyAction GetSSAction(Vector3 direction, float speed)
{
CCFlyAction action = ScriptableObject.CreateInstance<CCFlyAction>();
action.gravity = 9.8f;
action.time = 0;
action.speed = speed;
action.direction = direction;
return action;
}
public override void Start()
{
}
public override void Update()
{
time += (Time.deltaTime);
transform.Translate(Vector3.down * gravity * time * Time.deltaTime);
transform.Translate(direction * speed * Time.deltaTime);
//如果飞碟到达底部,则动作结束,进行回调
if (this.transform.position.y < -6)
{
this.destroy = true;
this.enable = false;
this.callback.SSActionEvent(this);
}
}
}
我们现在编写SSActionManager脚本,SSActionManager就是此次动作管理器实现的核心部分,其实现了对所有动作的管理,接收新创建的动作对象,并进行对已经存在的动作对象进行调度的功能,最后会将被标记为可删除的动作对象进行回收操作。在SSActionManager脚本中,我们用动作集、等待集与删除集管理动作,每次程序运行update()函数时先将waitingAdd中的动作保存,再运行被保存后的动作,运行后销毁该动作。对于下一次要执行的动作,先将动作初始化,然后保存在waitingAdd中,具体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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()
{
//将waitingAdd中的动作保存
foreach (SSAction ac in waitingAdd)
actions[ac.GetInstanceID()] = ac;
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();
}
}
//销毁waitingDelete中的动作
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
Destroy(ac);
}
waitingDelete.Clear();
}
//准备运行一个动作,将动作初始化,并加入到waitingAdd
public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
{
action.gameObject = gameObject;
action.transform = gameObject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
// Start is called before the first frame update
protected void Start()
{
}
}
我们现在编写CCActionManger脚本,由于动作管理器的设计不局限于一个项目的使用,因此在这个项目中继承 SSActionManager 再设计一个具体的CCActionManger类,使得其可以在这个项目中产生具体的动作实例,再进行调度操作。CCActionManger脚本负责生成飞行动作,并接受飞行动作的回调信息,使飞碟被回收具体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CCActionManager : SSActionManager, ISSActionCallback
{
//飞行动作
public CCFlyAction flyAction;
//控制器
public FirstController controller;
protected new void Start()
{
controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
controller.actionManager = this;
}
public void Fly(GameObject disk, float speed, Vector3 direction)
{
flyAction = CCFlyAction.GetSSAction(direction, speed);
RunAction(disk, flyAction, this);
}
//回调函数
public void SSActionEvent(SSAction source,
SSActionEventType events = SSActionEventType.Competed,
int intParam = 0,
string strParam = null,
Object objectParam = null)
{
//飞碟结束飞行后进行回收
controller.diskFactory.FreeDisk(source.gameObject);
}
}
我们现在编写场景控制器SceneController脚本,具体代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController
{
void LoadResources();
}
现在我们编写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;
}
}
现在我们编写主控制器MainController脚本,MainController脚本负责游戏主要逻辑,现在具体介绍一下几个函数。
Start()函数:
Start调用LoadReasources()函数。
LoadReasources()函数:
LoadReasources用于各个参数初始化。
SendDisk()函数:
SendDisk用于发射一个飞碟,首先从工厂获得一个飞碟,再为其设置初始位置和飞行动作。
Hit()函数:
Hit用于处理用户的点击动作,将用户点击到的飞碟移除,并计算分数。
GameStart()函数:
GameStart用于取得程序是否在游戏中的状态。
Restart()函数:
Restart用于重置游戏。
Update()函数:
Update用于发射飞碟与更新状态,飞碟每2s发射一次,每次做多3只,避免太过拥挤,当飞碟发射完毕后判断是否重置或者结束游戏。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public CCActionManager actionManager; //动作管理者
public DiskFactory diskFactory; //飞碟工厂
int[] roundDisks; //对应轮次的飞碟数量
bool isInfinite; //游戏当前模式
int points; //游戏当前分数
int round; //游戏当前轮次
int sendCnt; //当前已发送的飞碟数量
float sendTime; //发送时间
bool gameStart;
void Start()
{
LoadResources();
}
public void LoadResources()
{
SSDirector.GetInstance().CurrentScenceController = this;
gameObject.AddComponent<DiskFactory>();
gameObject.AddComponent<CCActionManager>();
gameObject.AddComponent<UserGUI>();
diskFactory = Singleton<DiskFactory>.Instance;
sendCnt = 0;
round = 0;
sendTime = 0;
points = 0;
isInfinite = false;
roundDisks = new int[] { 1, 2, 4 ,8 ,16};
}
public void SendDisk()
{
//从工厂生成一个飞碟
GameObject disk = diskFactory.GetDisk(round);
//设置飞碟的随机位置
disk.transform.position = new Vector3(-disk.GetComponent<DiskData>().direction.x * 7, UnityEngine.Random.Range(0f, 8f), 0);
disk.SetActive(true);
//设置飞碟的飞行动作
actionManager.Fly(disk, disk.GetComponent<DiskData>().speed, disk.GetComponent<DiskData>().direction);
}
public void Hit(Vector3 position)
{
Camera ca = Camera.main;
Ray ray = ca.ScreenPointToRay(position);
RaycastHit[] hits;
hits = Physics.RaycastAll(ray);
for (int i = 0; i < hits.Length; i++)
{
RaycastHit hit = hits[i];
if (hit.collider.gameObject.GetComponent<DiskData>() != null)
{
//将飞碟移至底端,触发飞行动作的回调
hit.collider.gameObject.transform.position = new Vector3(0, -7, 0);
//积分
points += hit.collider.gameObject.GetComponent<DiskData>().points;
//更新GUI数据
gameObject.GetComponent<UserGUI>().points = points;
}
}
}
public void GameStart(bool Mystart)
{
gameStart = Mystart;
}
public void Restart()
{
gameObject.GetComponent<UserGUI>().gameMessage = "";
round = 0;
sendCnt = 0;
points = 0;
gameObject.GetComponent<UserGUI>().points = points;
}
public void SetMode(bool isInfinite)
{
this.isInfinite = isInfinite;
}
void Update()
{
if(gameStart)
{
sendTime += Time.deltaTime;
//每隔1s发送一次飞碟
if (sendTime > 2)
{
sendTime = 0;
//每次发送至多3个飞碟
for (int i = 0; sendCnt < roundDisks[round]; i++)
{
sendCnt++;
SendDisk();
}
//判断是否需要重置轮次,不需要则输出游戏结束
if (sendCnt == roundDisks[round] && round == roundDisks.Length - 1)
{
if (isInfinite)
{
round = 0;
sendCnt = 0;
gameObject.GetComponent<UserGUI>().gameMessage = "";
}
else
{
gameObject.GetComponent<UserGUI>().gameMessage = "Game Over!";
}
}
//更新轮次
if (sendCnt == roundDisks[round] && round < roundDisks.Length - 1)
{
sendCnt = 0;
round++;
}
}
}
}
}
到此,所有脚本编写完毕,可以对游戏进行游玩了。