游戏内容要求:
1.靶对象为 5 环,按环计分;
2.箭对象,射中后要插在靶上
- 增强要求:射中后,箭对象产生颤抖效果,到下一次射击 或 1秒以后
3.游戏仅一轮,无限 trials;
- 增强要求:添加一个风向和强度标志,提高难度
游戏规则
1.按空格键取箭,然后按住鼠标左键同时移动鼠标,箭头会跟着鼠标移动,此时松开左键,箭就会射出。射中后箭体会进行抖动。
2.靶上一共有5环,击中n环加n分,即5环加50分。
3.左上角会提示当前风力方向和强度,会影响箭的飞行轨迹。
游戏实现
- 首先是预制体靶和箭的设置。
靶上面要有5环,因此我在一个空对象Target下面创建了5个Cylinder子对象,各自带上Mesh Collider网格碰撞器,并且对Mesh Collider的Convex选项打勾,即为凸的网格,这样才能跟其他碰撞器产生碰撞作用。
对于箭的设置来说,首先在一个空对象Arrow下创建一个柱体Cylinder和正方体Cube构成箭身和箭头。给空对象Arrow加上刚体Rigidbody并且勾选Is Kinematic,即开始时候为运动学刚体;给箭身加碰撞器,箭头也加上加碰撞器并且勾选Is Trigger,同时挂载检测碰撞的脚本。
- 然后是动作管理的模块,总体来说与UFO动作管理类似,所以先把大的框架搬过来,然后对于箭飞行的部分ArrowFlyAction进行实现。因为在鼠标的拖动下实现箭的射击,所以得到鼠标的方向后将方向乘30作为射箭的力的大小进行输入,然后调用AddForce函数将箭射出。
public class ArrowFlyActionManager : SSActionManager
{
private ArrowFlyAction fly;
public controllor scene_controller;
protected void Start()
{
scene_controller = (controllor)SSDirector.GetInstance().CurrentScenceController;
scene_controller.action_manager = this;
}
public void ArrowFly(GameObject arrow,Vector3 wind,Vector3 f){
fly = ArrowFlyAction.GetSSAction(wind, f);
this.RunAction(arrow, fly, this);
}
}
public class ArrowFlyAction : SSAction
{
public Vector3 force;
public Vector3 wind;
private ArrowFlyAction() { }
public static ArrowFlyAction GetSSAction(Vector3 wind,Vector3 force_)
{
ArrowFlyAction action = CreateInstance<ArrowFlyAction>();
action.force = force_;
action.wind = wind;
return action;
}
public override void Update(){}
public override void FixedUpdate()
{
this.gameobject.GetComponent<Rigidbody>().AddForce(wind, ForceMode.Force);
if (this.transform.position.z > 30 || this.gameobject.tag == "hit"){
this.destroy = true;
this.callback.SSActionEvent(this,this.gameobject);
}
}
public override void Start(){
gameobject.transform.LookAt(force);
gameobject.GetComponent<Rigidbody>().isKinematic = false;
gameobject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
gameobject.GetComponent<Rigidbody>().velocity = Vector3.zero;
gameobject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
}
}
- 接着是场景控制器的设计。
场景的控制器继承了IUserAction接口,并实现其方法。箭由箭工厂产生,通过create函数调用工厂方法。在Update()方法让箭工厂检测回收掉地的箭。在shoot函数中为箭的发射添加一个冲力Impulse。为了使箭头也朝着飞出方向,所以调用transform.LookAt方法改变朝向箭飞出时获取当前风力方向和强度,然后调用动作管理类的函数给箭加一个力,使其飞行。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class controllor : MonoBehaviour, IUserAction, ISceneController
{
public scoreRecorder recorder;
public arrowFactory arrow_factory;
public ArrowFlyActionManager action_manager;
public UserGUI user_gui;
private GameObject arrow;
private GameObject target;
private bool game_start = false;
private string wind = "";
private float wind_directX;
private float wind_directY;
public int GetScore(){return recorder.score;}
public string GetWind(){return wind;}
public void Restart(){SceneManager.LoadScene(0);}
public void BeginGame(){game_start = true;}
void Update (){if(game_start) arrow_factory.FreeArrow ();}
public void LoadResources(){target = Instantiate(Resources.Load("Prefabs/target", typeof(GameObject))) as GameObject;}
public bool haveArrowOnPort() {return (arrow != null);}
void Start ()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this;
arrow_factory = singleton<arrowFactory>.Instance;
recorder = gameObject.AddComponent<scoreRecorder>() as scoreRecorder;
user_gui = gameObject.AddComponent<UserGUI> () as UserGUI;
action_manager = gameObject.AddComponent<ArrowFlyActionManager>() as ArrowFlyActionManager;
LoadResources();
}
public void create(){
if (arrow == null) {
wind_directX = Random.Range(-4, 4);
wind_directY = Random.Range(-4, 4);
CreateWind();
arrow = arrow_factory.GetArrow ();
}
}
public void MoveBow(Vector3 mousePos)
{
if (!game_start){return;}
arrow.transform.LookAt(mousePos * 30);
}
public void Shoot(Vector3 mousePos)
{
if(game_start)
{
Vector3 wind = new Vector3(wind_directX, wind_directY, 0);
action_manager.ArrowFly(arrow, wind,mousePos*30);
arrow = null;
}
}
public void CreateWind()
{
string Horizontal = "", Vertical = "", level = "";
if (wind_directX > 0)
{
Horizontal = "西";
}
else if (wind_directX <= 0)
{
Horizontal = "东";
}
if (wind_directY > 0)
{
Vertical = "南";
}
else if (wind_directY <= 0)
{
Vertical = "北";
}
if ((wind_directX + wind_directY) / 2 > -1 && (wind_directX + wind_directY) / 2 < 1)
{
level = "1 级";
}
else if ((wind_directX + wind_directY) / 2 > -2 && (wind_directX + wind_directY) / 2 < 2)
{
level = "2 级";
}
else if ((wind_directX + wind_directY) / 2 > -3 && (wind_directX + wind_directY) / 2 < 3)
{
level = "3 级";
}
else if ((wind_directX + wind_directY) / 2 > -5 && (wind_directX + wind_directY) / 2 < 5)
{
level = "4 级";
}
wind = Horizontal + Vertical + "风" + " " + level;
}
}
- 接着实现箭的工厂类,实现了制造箭和回收箭。
同样,设置一个usingList和unusedList,进行箭的循环利用。由场景控制器中的Update()方法让箭工厂每帧检测回收掉地的箭。箭掉地后,复位。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class arrowFactory : MonoBehaviour {
public GameObject arrow = null;
private List<GameObject> usingArrowList = new List<GameObject>();
private Queue<GameObject> unusedArrowList = new Queue<GameObject>();
public controllor sceneControler;
public GameObject GetArrow()
{
if (unusedArrowList.Count == 0) arrow = Instantiate(Resources.Load<GameObject>("Prefabs/arrow"));
else
{
arrow = unusedArrowList.Dequeue();
arrow.gameObject.SetActive(true);
}
arrow.transform.position = new Vector3 (0, 0, 0);
usingArrowList.Add(arrow);
return arrow;
}
public void FreeArrow()
{
for (int i = 0; i < usingArrowList.Count; i++) {
if (usingArrowList[i].transform.position.y <= -8||usingArrowList[i].transform.position.y >= 8) {
usingArrowList[i].GetComponent<Rigidbody>().isKinematic = true;
usingArrowList[i].SetActive(false);
usingArrowList[i].transform.position = new Vector3 (0, 0, 0);
unusedArrowList.Enqueue(usingArrowList[i]);
usingArrowList.Remove(usingArrowList[i]);
i--;
}
}
}
}
- 然后就是最重要的碰撞检测类
此类挂载在箭头上。由于上面设置了箭头Is Trigger为true,即不会产生碰撞效果,所以永远不会触发OnCollisionEnter方法。因此,需要将碰撞进入方法调用void OnTriggerEnter。为了防止多次触发OnTriggerEnter方法,需要设置箭头为inactive。由于触碰后整个箭组合会变为运动学刚体而静止,所以箭身会插在靶上。最后根据射中的位置与中心点距离确定环数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class arrowCollider : MonoBehaviour
{
public controllor scene_controller;
public scoreRecorder recorder;
void Start()
{
scene_controller = SSDirector.GetInstance().CurrentScenceController as controllor;
recorder = singleton<scoreRecorder>.Instance;
}
void OnTriggerEnter(Collider c)
{
if (c.gameObject.name == "T1"||c.gameObject.name == "T2"||c.gameObject.name == "T3"||c.gameObject.name == "T4"||c.gameObject.name == "T5") {
gameObject.transform.parent.gameObject.GetComponent<Rigidbody>().isKinematic = true;
gameObject.SetActive(false);
float point = Mathf.Sqrt (this.gameObject.transform.position.x * this.gameObject.transform.position.x + this.gameObject.transform.position.y * this.gameObject.transform.position.y);
recorder.Record(5-(int)Mathf.Floor(point*2));
}
}
}
- 然后是箭中靶后的抖动部分
箭中靶后,通过回调函数告诉动作管理器,去执行箭颤抖动作。箭颤抖是通过短时间内上下快速移动实现的。
public class ArrowTremble : SSAction
{
float radian = 0;
float per_radian = 3f;
float radius = 0.01f;
Vector3 old_pos;
public float left_time = 0.8f;
private ArrowTremble() { }
public override void Start(){old_pos = transform.position;}
public static ArrowTremble GetSSAction(){
ArrowTremble action = CreateInstance<ArrowTremble>();
return action;
}
public override void Update(){
left_time -= Time.deltaTime;
if (left_time <= 0){
transform.position = old_pos;
this.destroy = true;
this.callback.SSActionEvent(this);
}
radian += per_radian;
float dy = Mathf.Cos(radian) * radius;
transform.position = old_pos + new Vector3(0, dy, 0);
}
public override void FixedUpdate(){}
}
- 接下来是GUI部分。
UserGUI中需要每帧读取鼠标移动的位置来传给场景控制器进行箭的移动或者发射。也需要检测是否正确按下空格取箭。其他的UI部分与上次类似。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour {
private IUserAction action;
GUIStyle style = new GUIStyle();
GUIStyle style2 = new GUIStyle();
private bool game_start = false;
void Start ()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
style.normal.textColor = new Color(0, 0, 0, 1);
style.fontSize = 16;
style2.normal.textColor = new Color(1, 1, 1);
style2.fontSize = 25;
}
void Update()
{
if(game_start)
{
if (action.haveArrowOnPort ()) {
if (Input.GetMouseButton(0)) {
Vector3 mousePos = Camera.main.ScreenPointToRay(Input.mousePosition).direction;
action.MoveBow(mousePos);
}
if (Input.GetMouseButtonUp(0)) {
Vector3 mousePos = Camera.main.ScreenPointToRay(Input.mousePosition).direction;
action.Shoot(mousePos);
}
}
if (Input.GetKeyDown(KeyCode.Space)) action.create();
}
}
private void OnGUI()
{
if(game_start)
{
GUI.Label(new Rect(10, 5, 200, 50), "分数:", style);
GUI.Label(new Rect(55, 5, 200, 50), action.GetScore().ToString(), style);
GUI.Label(new Rect(Screen.width - 170, 30, 200, 50), "风向: ", style);
GUI.Label(new Rect(Screen.width - 110, 30, 200, 50), action.GetWind(), style);
if (GUI.Button(new Rect(Screen.width - 170, 0, 100, 30), "重新开始"))
{
action.Restart();
return;
}
}
else
{
GUI.Label(new Rect(Screen.width / 2 - 80, Screen.width / 2 - 320, 100, 100), "Arrow Shooting", style2);
if (GUI.Button(new Rect(Screen.width / 2 - 50, Screen.width / 2 - 150, 100, 50), "游戏开始"))
{
game_start = true;
action.BeginGame();
}
}
}
}
- 然后是接口类,声明了需要在场景控制类中实现的函数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController
{
void LoadResources();
}
public interface IUserAction
{
void MoveBow(Vector3 mousePos);
void Shoot (Vector3 mousePos);
int GetScore();
void Restart();
string GetWind();
void BeginGame();
void create ();
bool haveArrowOnPort();
}
最后是计分器类以及导演类、单例类等框架,与前几次基本相同。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class scoreRecorder : MonoBehaviour {
public int score;
void Start(){score = 0;}
public void Record(int point){score = point + score;}
}
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;
}
}
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;
}
}
}
最后在屏幕中加上一个小的摄像头,可以看到箭射中靶心的情况。
这样就完成了打靶游戏的设计。
四.实验心得与总结。
总的来说3D作业越来越让我认识到设计的过程中一个好的架构的重要性。几次作业中使用了相同的MVC架构,并且将动作分离,这样加设计的过程中就可以将游戏的每个部分分开来设计,最后通过架构组合到一起,这时的设计的过程中每个部分的作用都很清晰,出现的问题也可以有针对性地解决,十分强大.
视频展示:打靶游戏
GitHub地址:ArrowShooting
最后感谢师兄的博客!