作业六:打靶游戏
1:游戏要求:
1.1游戏内容要求:
- 靶对象为 5 环,按环计分;
- 箭对象,射中后要插在靶上
- 增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
- 游戏仅一轮,无限 trials;
- 增强要求:添加一个风向和强度标志,提高难度
1.2游戏编程要求
- 使用Adapter模式:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
2:游戏对象预制:
2.1:靶子Target:
靶子是由五个同心的圆柱体组成的预制体,每个圆柱体代表一个环,每个环的分值不一样。并且每个圆柱体都需要添加MeshCollider组件,并设置为IsTrigger,用来检测箭是否中靶。详细设置:
2.2:箭Arrow:
箭主要有两部分,箭身以及子对象箭头,设置标签为arrow详细设置如下图:
- 箭身的设置:
- 箭头的设置:
2.3: 弓bow:
弓同样由两部分组成,弓本身以及一个架在弓上的箭,所以需要添加一个箭的预制:
3:编程:
3.1 Adapter模式:
1:基类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; //动作完成后的消息通知者
protected SSAction() { }
//子类可以使用下面这两个函数
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
public virtual void FixedUpdate()
{
throw new System.NotImplementedException();
}
}
2:箭的射击动作 ArrowShootAction.cs
通过一个作为初始动力的冲量pulseForce和一个持续的风力windForce驱使箭飞行。在箭中靶或者脱靶,即飞行动作结束之后,回调箭的颤抖动作:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowShootAction : SSAction
{
private Vector3 pulseForce; //射箭提供的冲量
private Vector3 windForce; //风力
private ArrowShootAction(){}
public static ArrowShootAction GetSSAction(Vector3 wind)
{
ArrowShootAction shootarrow = CreateInstance<ArrowShootAction>();
shootarrow.pulseForce = new Vector3(0,0,25);
shootarrow.windForce = wind;
return shootarrow;
}
// Start is called before the first frame update
public override void Start()
{
//摆脱跟随弓移动
gameobject.transform.parent = null;
gameobject.GetComponent<Rigidbody>().velocity = Vector3.zero;
//添加初始的冲量
gameobject.GetComponent<Rigidbody>().AddForce(pulseForce,ForceMode.Impulse);
//关闭运动学控制
gameobject.GetComponent<Rigidbody>().isKinematic = false;
}
// Update is called once per frame
public override void Update()
{
}
public override void FixedUpdate(){
//添加风力
this.gameobject.GetComponent<Rigidbody>().AddForce(windForce,ForceMode.Force);
//射击动作结束,即中靶或者脱靶,回调颤抖动作
if(this.transform.position.z > 35 || this.gameobject.tag == "onTarget" )
{
this.destroy = true;
this.callback.SSActionEvent(this,1,this.gameobject);
}
}
}
3:ArrowTrembleAction 箭的颤抖动作:
围绕箭的头部进行细微的运动,实现颤抖的效果,该动作在结束时也会调用回调函数,不过没有下一步的动作,所以回调的函数为空;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowTrembleAction : SSAction
{
float tremble_radius = 1.2f;//颤抖的程度
float tremble_time = 1.0f; //颤抖的时间
Vector3 arrow_pos; //箭的原位置
private ArrowTrembleAction(){}
//放回一个颤抖的动作
public static ArrowTrembleAction GetSSAction()
{
ArrowTrembleAction tremble_action = CreateInstance<ArrowTrembleAction>();
return tremble_action;
}
// Start is called before the first frame update
public override void Start()
{
//得到箭中靶时的位置
arrow_pos = this.transform.position;
}
//实现箭的颤抖动作
// Update is called once per frame
public override void Update()
{ //更新时间,得到剩余的颤抖时间
tremble_time -= Time.deltaTime;
//需要继续颤抖
if(tremble_time > 0){
///获取头部的位置
Vector3 head_pos = this.transform.GetChild(0).position;
//围绕箭头颤抖
this.transform.RotateAround(head_pos,tremble_radius);
}
else{
//将箭返回一开始的位置
transform.position = arrow_pos;
//开始销毁动作
this.destroy = true;
//通过接口回调,调用新的动作,实际上没有新的动作,回调为空
this.callback.SSActionEvent(this);
}
}
public override void FixedUpdate()
{
}
}
4:SSActionManage统一的动作管理接口:
该类维护着一个动作队列actions,在每次更新时,即Update()或者FixedUpdate(),都会添加新的动作,或者删除需要销毁的动作,并运行需要更新的动作;同时提供了一个添加新动作的方法RunAction和回调接口SSActionEvent;
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>();
// Start is called before the first frame update
// Update is called once per frame
protected void Update()
{
//添加新增的动作
for(int i = 0; i < waitingAdd.Count; ++i)
{
actions[waitingAdd[i].GetInstanceID()] = waitingAdd[i];
}
waitingAdd.Clear();
//销毁需要删除的动作
foreach (KeyValuePair<int,SSAction> kv in actions){
SSAction t_ac = kv.Value;
if(t_ac.destroy)
{
waitingDelete.Add(t_ac.GetInstanceID());
}
//动作不需要销毁则更新
else if(t_ac.enable)
{
t_ac.Update();
}
}
//销毁动作
foreach (int key in waitingDelete){
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
waitingDelete.Clear();
}
protected void FixedUpdate()
{
for(int i = 0; i < waitingAdd.Count; ++i)
{
actions[waitingAdd[i].GetInstanceID()] = waitingAdd[i];
}
waitingAdd.Clear();
foreach (KeyValuePair<int,SSAction> kv in actions){
SSAction t_ac = kv.Value;
if(t_ac.destroy)
{
waitingDelete.Add(t_ac.GetInstanceID());
}
else if(t_ac.enable)
{
t_ac.FixedUpdate();
}
}
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();
}
//回调新的动作类型
public void SSActionEvent(SSAction source, int param = 0,GameObject arrow = null)
{
//执行完飞行动作,开始颤抖动作。
if(param == 1)
{
ArrowTrembleAction tremble = ArrowTrembleAction.GetSSAction();
RunAction(arrow,tremble,this);
}
else{
}
}
}
5:ArrowShootActionManager,提供一个接口用于执行箭的射击动作;
该类继承了SSActionManager,提供了一个方法ArrowShoot,这个方法调用父类的RunAction方法执行射击动作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//
public class ArrowShootActionManager : SSActionManager
{
//箭飞行的动作
private ArrowShootAction shoot;
public FirstSceneController scene_controller; //当前场景的场景控制器
protected void Start()
{
//设置firstscenecontroller的arrow——manager
scene_controller = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
scene_controller.arrow_manager = this;
}
//箭飞行
public void ArrowShoot(GameObject arrow,Vector3 wind)
{
//实例化一个飞行动作。
shoot = ArrowShootAction.GetSSAction(wind);
//调用SSActionmanager的方法运行动作。
this.RunAction(arrow, shoot, this);
}
}
3.2 SSDirector 导演类
提供一个单实例,用来实现界面的统一管理,此游戏只需要一个界面。
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;
}
}
3.3:Arrow Factory 工厂
用来管理箭的生产和回收,主要提供两个方法,GetArrow()和RecycleArrow()分别用来得到空闲的箭和回收重复利用使用过的箭:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ArrowFactory : MonoBehaviour
{
//空闲队列
private Queue<GameObject> FreeArrow = new Queue<GameObject>();
//使用的箭的队列
private List<GameObject> UsedArrow = new List<GameObject>();
public FirstSceneController scenecontrolller;
public GameObject arrow = null;
public GameObject GetArrow(){
if(FreeArrow.Count == 0){
arrow = Instantiate(Resources.Load<GameObject>("Prefabs/arrow"));
}
else{
arrow = FreeArrow.Dequeue();
if(arrow.tag == "onTarget")//箭在靶子上
{//使用动力学控制
arrow.GetComponent<Rigidbody>().isKinematic = false;
arrow.tag = "arrow";
arrow.transform.GetChild(0).gameObject.SetActive(true);
}
arrow.gameObject.SetActive(true);
}
scenecontrolller = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
//得到弓箭上搭箭的位置
Transform bow_mid = scenecontrolller.bow.transform.GetChild(0);
//将箭的位置设置为弓中间的位置
arrow.transform.position = bow_mid.transform.position;
//箭随弓的位置变化
arrow.transform.parent = scenecontrolller.bow.transform;
UsedArrow.Add(arrow);
return arrow;
}
public void RecycleArrow(GameObject arrow)
{
for(int i = 0 ; i < UsedArrow.Count; ++i)
{
if(arrow.GetInstanceID() == UsedArrow[i].gameObject.GetInstanceID())///
{
//UsedArrow[i].gameObject.SetActive(false);
FreeArrow.Enqueue(UsedArrow[i]);
UsedArrow.RemoveAt(i);
break;
}
}
}
}
3.4 相机脚本:
1:CameraFlow实现主相机跟随:
将相机的位置与弓的位置之间维持固定的位移偏差offset,即随着弓移动:
using UnityEngine;
using System.Collections;
public class CameraFlow : MonoBehaviour
{
public GameObject bow; //跟随的物体
public float smothing = 5f; //相机跟随的速度
Vector3 offset; //相机与物体相对偏移位置
void Start()
{
//设置相机与弓箭的位移差
offset = new Vector3(2,0,-6);
}
void FixedUpdate()
{
//相机的目标位置
Vector3 target = bow.transform.position + offset;
//摄像机自身位置到目标位置平滑过渡
transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
}
}
2:SecondeCamera:副相机脚本
在小窗口更加详细的展示箭的中靶位置和箭的颤抖动作,设置的展示时间为每次射击后的三秒钟:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SecondCamera : MonoBehaviour
{
public float show_time = 3f;
public bool show = false;
void Update(){
if(show == true){
show_time -= Time.deltaTime;
//展示时间超过3秒,停止展示
if(show_time < 0){
show = false;
this.gameObject.SetActive(false);
}
}
}
//展示服相机
public void ShowCamera()
{
show = true;
this.gameObject.SetActive(true);
show_time = 3f;
}
}
3.5 RingController 环的脚本:
主要有一个成员变量RingScore用来标记当前环的分值,需要在Inspector窗口设置相应的分值;还有一个触发器检测,用来检查是否有箭击中该靶。如果有箭中靶,需要更改标签,以便调用相应的动作:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RingController : MonoBehaviour
{
//当前环的分值
public int RingScore = 0;
public ISceneController scene;
public ScoreRecorder sc_recorder;
// Start is called before the first frame update
void Start()
{
scene = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
sc_recorder = Singleton<ScoreRecorder>.Instance;
}
// Update is called once per frame
void Update()
{
}
//碰撞检测,如果箭击中该环,就响应。
void OnTriggerEnter(Collider arrow_head){
//得到箭身
Transform arrow = arrow_head.gameObject.transform.parent;
if(arrow == null)
{
return ;
}
//有箭中靶
if(arrow.tag == "arrow"){
//将箭的速度设为0
arrow.GetComponent<Rigidbody>().velocity = new Vector3(0,0,0);
//使用运动学运动控制
arrow.GetComponent<Rigidbody>().isKinematic = true;
//计分
sc_recorder.RecordScore(RingScore);
//将箭头设置为消失,
arrow_head.gameObject.SetActive(false);/
//标记箭为中靶
arrow.tag = "onTarget";
}
}
}
3.6 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;
}
}
}
3.7 Interface接口:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController{
void LoadResources();
}
//箭的飞行和颤抖动作的回调接口
public interface ISSActionCallback
{
void SSActionEvent(SSAction source,int param = 0, GameObject arrow = null);
}
3.8 玩家交互界面:
显示相应的信息,以及响应玩家的动作,如点击开始游戏的按钮,和通过方向键控制弓的移动,点击空格箭发射。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
//private IUserAction action;
private FirstSceneController action;
GUIStyle tip_style = new GUIStyle();
GUIStyle wind_style = new GUIStyle();
GUIStyle button_style = new GUIStyle();
GUIStyle state_style = new GUIStyle();
private int game_state = 0; //1-》游戏结束 0->游戏正在进行
// Start is called before the first frame update
void Start()
{
action = this.transform.gameObject.GetComponent<FirstSceneController>();
tip_style.fontSize = 16;
tip_style.normal.textColor = new Color(1,1,1,1);
wind_style.normal.textColor = new Color(1,0.8f,0.8f,1);
wind_style.fontSize = 16;
button_style.normal.textColor = new Color(1,0.5f,0.7f,1);
button_style.fontSize = 20;
state_style.normal.textColor = new Color(0.9f,0.7f,0.5f,1);
state_style.fontSize = 30;
game_state = 0;//游戏状态 0->未开始 1-》开始
}
// Update is called once per frame
//获取方向键的偏移量并移动弓
void Update()
{
if(game_state == 1)
{
//点击空格键射击
if(Input.GetKeyDown(KeyCode.Space))
{
action.ShootArrow();
}
//获取方向键的位移
float transY = Input.GetAxis("Vertical");
float transX = Input.GetAxis("Horizontal");
//移动弓相应的位移
action.MoveBow(transX,transY); //
}
}
//
private void OnGUI()
{//游戏已经开始
if(game_state == 1)
{ //游戏正在进行
if(action.GetGameState() == 1) //
{
//显示各种信息
GUI.Label(new Rect(20,10,150,50),"Score: ",button_style);
GUI.Label(new Rect(100,10,100,50),action.GetScore().ToString(),state_style);
GUI.Label(new Rect(200,10,100,50),"wind: ",wind_style);
Vector3 wind = action.GetWind();
string wind_data = "(" + wind.x +"," + wind.y + ",0)";
GUI.Label(new Rect(250,10,100,50),wind_data,wind_style);
GUI.Label(new Rect(20,50,150,50),"ArrowNum: ",button_style);
GUI.Label(new Rect(120,50,100,50),action.GetArrowNum().ToString(),button_style);
//重新开始的按钮
if (GUI.Button(new Rect(80,100, 100, 50), "重新开始"))
{
action.Restart();
}
}
else{
GUI.Label(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 250, 100, 100), "游戏结束", button_style);
}
}
//游戏未开始,检测开始按键
else{
GUI.Label(new Rect(Screen.width / 2 - 150, Screen.width / 2 - 370, 100, 100), "Bowman", wind_style);
//开始按键如果按下,游戏开始
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "游戏开始"))
{
game_state = 1;
action.BeginGame();
}
GUI.Label(new Rect(Screen.width / 2 - 100, Screen.width / 2 - 320, 400, 100), "使用方向键控制弓箭移动,点击空格键射箭", wind_style);
}
}
}
3.9 FirstSceneController 界面管理:
获取所需要的资源,响应玩家交互类传过来的动作,更新风向,分数等信息,调用箭的动作等等。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstSceneController : MonoBehaviour,ISceneController
{
//箭的动作管理者
public ArrowShootActionManager arrow_manager;
//箭的工厂
public ArrowFactory factory;
//副相机
public Camera second_camera;
//主相机
public Camera main_camera;
//记录分数
public ScoreRecorder recorder;
//弓
public GameObject bow;
//靶
public GameObject target;
//风
public Vector3 wind;
//箭
public GameObject arrow;
private int arrow_num = 0; //射出的箭数
//箭的队列
private List<GameObject> arrows = new List<GameObject>();
private int gameState = 0; //0-游戏未开始,1-》游戏进行
//加载箭和靶子
public void LoadResources()
{
bow = Instantiate(Resources.Load("Prefabs/bow",typeof(GameObject))) as GameObject;
target = Instantiate(Resources.Load("Prefabs/target",typeof(GameObject))) as GameObject;
}
//进行资源加载
// Start is called before the first frame update
void Start()
{
SSDirector director = SSDirector.GetInstance();
factory = Singleton<ArrowFactory>.Instance;
recorder = Singleton<ScoreRecorder>.Instance;
director.CurrentScenceController = (ISceneController)this;
arrow_manager = this.gameObject.AddComponent<ArrowShootActionManager>() as ArrowShootActionManager;
LoadResources();
main_camera.GetComponent<CameraFlow>().bow = bow;
float windx = Random.Range(-2,2);
float windy = Random.Range(-2,2);
wind = new Vector3(windx,windy,0);
}
// Update is called once per frame
void Update()
{
if(gameState == 1)
{
for(int i = 0; i < arrows.Count; ++i)
{ //当前界面如果箭飞出靶外或者数量大于3则回收
GameObject t_arrow = arrows[i];
if(t_arrow.transform.position.z > 35 || arrows.Count > 3)
{
factory.RecycleArrow(t_arrow);
arrows.RemoveAt(i);
}
}
}
}
//通过方向键移动弓
public void MoveBow(float transX, float transY)
{ //限制弓的位置在一定范围内,x,y的绝对值小于5,z为-5;
if(gameState == 1)
{
bow.transform.position = new Vector3(bow.transform.position.x,bow.transform.position.y,-5);
if(bow.transform.position.x > 5)
{
bow.transform.position = new Vector3(5,bow.transform.position.y,bow.transform.position.z);
return;
}
if(bow.transform.position.x < -5)
{
bow.transform.position = new Vector3(-5,bow.transform.position.y,bow.transform.position.z);
return;
}
if(bow.transform.position.y > 5)
{
bow.transform.position = new Vector3(bow.transform.position.x,5,bow.transform.position.z);
return;
}
if(bow.transform.position.y < -5)
{
bow.transform.position = new Vector3(bow.transform.position.x,-5,bow.transform.position.z);
return;
}
//移动特定的距离
bow.transform.Translate(new Vector3(0,transY,-transX)*Time.deltaTime);
}
}
//射箭
public void ShootArrow()
{
if(gameState == 1)
{ //从工厂得到箭
arrow = factory.GetArrow();
arrows.Add(arrow);
//通过管理类发射动作
arrow_manager.ArrowShoot(arrow,wind);
//开启副相机
second_camera.GetComponent<SecondCamera>().ShowCamera();
arrow_num += 1;
//每次射击后风向改变;
float windx = Random.Range(-2,2);
float windy = Random.Range(-2,2);
wind = new Vector3(windx,windy,0);
}
}
//返回当前分数
public int GetScore()
{
return recorder.score;
}
//得到风向
public Vector3 GetWind()
{
return wind;
}
//得到射出的箭的数量
public int GetArrowNum()
{
return arrow_num;
}
//重新开始游戏
public void Restart()
{
arrow_num = 0;
recorder.score = 0;
for(int i = 0; i < arrows.Count; ++i){
factory.RecycleArrow(arrows[i]);
}
arrows.Clear();
}
//开始游戏
public void BeginGame()
{
gameState = 1;
}
//得到游戏的状态
public int GetGameState()
{
return gameState;
}
}
4:运行游戏:
4.1: 相机
需要添加一个新的相机作为副相机,需要设置副相机Viewport Rect属性,设置为小窗口显示:并添加脚本SecondCamera,同时主相机也需要添加脚本CameraFlow:
4.2 GameObject
创建一个空对象,并添加和设置相应脚本:
4.3: Target:
每个环都要添加RingController,并设置RingScore;
5: 游戏展示:
- 一共有五环,分数分别为1,2,3,4,5;
- 方向键移动弓的位置;
- 空格箭发射
Unity3D射箭
6:项目地址:
点击Scenes/BowAndArrow可运行项目;