游戏规则:
1、按空格键取箭,然后按住鼠标左键同时移动鼠标,箭头会跟着鼠标移动,此时松开左键,箭就会射出。
2、靶上一共有5环,击中n环加n*10分,即5环加50分
3、左上角会提示当前风力方向和强度,会影响箭的飞行轨迹
游戏效果:
游戏UML类图:
此次作业的项目结构与实现思路都跟上一次打飞碟游戏比较类似,可以参照着看哦~
难点实现思路:
这里先说一下作业的一些难点的实现思路:
1、靶标的实现:
靶上面要有5环,因此我在一个空对象Target下面创建了5个Cylinder子对象,各自带上Mesh Collider网格碰撞器。(注意要对Mesh Collider勾上Convex选项,即为凸的网格,才能跟其他碰撞器产生碰撞作用)。因为5个叠在一起,可能显示上会有先后顺序,此时改一下z坐标即可,即内环比外环前一点点:
2、箭的刚体、碰撞器组合情况:
我在一个空对象Arrow下创建一个柱体Cylinder和正方体Cube构成箭身和箭头。给空对象Arrow加上刚体Rigidbody(勾选Is Kinematic,即开始时候为运动学刚体),给箭身加碰撞器,箭头加碰撞器(但需要勾选Is Trigger,同时挂载检测碰撞的脚本)。
3、增加风力:
加一个普通的带方向的力即可。
代码解释:
1、SceneController.cs
单例场景控制类,实现IUserAction、IGameStatusOp两个接口。此类有两个子对象:GameModel(负责管理场景内的各个游戏对象)、GameStatus(负责管理场景各种状态)。因此SceneController类实现接口的方法均为直接调用两个子对象的public方法:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Com.Shooting {
public interface IUserAction {
void createArrow();
void arrowFollowMouse(Vector3 mousePos);
void shootTheArrow(Vector3 mousePos);
}
public interface IGameStatusOp {
bool haveArrowOnPort();
void addScore(int point);
void showTipsText(int point);
void modifyWind();
int getWindDir();
int getWindStrength();
}
public class SceneController : System.Object, IUserAction, IGameStatusOp {
private static SceneController instance;
private GameModel myGameModel;
private GameStatus myGameStatus;
public static SceneController getInstance() {
if (instance == null)
instance = new SceneController();
return instance;
}
internal void setGameModel(GameModel _myGameModel) {
if (myGameModel == null) {
myGameModel = _myGameModel;
}
}
internal void setGameStatus(GameStatus _myGameStatus) {
if (myGameStatus == null) {
myGameStatus = _myGameStatus;
}
}
/********************* 实现IUserAction接口 ************************/
public void createArrow() {
myGameModel.createArrow();
}
public void arrowFollowMouse(Vector3 mousePos) {
myGameModel.arrowFollowMouse(mousePos);
}
public void shootTheArrow(Vector3 mousePos) {
myGameModel.shootTheArrow(mousePos);
}
/********************* 实现IGameStatusOp接口 ************************/
public bool haveArrowOnPort() {
return myGameModel.haveArrowOnPort();
}
public void addScore(int point) {
myGameStatus.addScore(point);
}
public void showTipsText(int point) {
myGameStatus.showTipsText(point);
}
public void modifyWind() {
myGameStatus.modifyWind();
}
public int getWindDir() {
return myGameStatus.getWindDir();
}
public int getWindStrength() {
return myGameStatus.getWindStrength();
}
}
}
2、 GameModel.cs
上面说了,这是场景控制类SceneController的子对象,负责管理箭、靶两个游戏对象的行为逻辑。
(1)箭由箭工厂产生;Update()方法让箭工厂检测回收掉地的箭
(2)箭的发射为添加一个冲力Impulse。但是由于未射箭时需要跟着鼠标移动,而设置为运动学刚体,不受物理作用,所以发射瞬间需要先设置为非运动学刚体(**.isKinematic = false)。为了使箭头也朝着飞出方向,所以调用transform.LookAt方法改变朝向
(3)箭飞出时获取当前风力方向和强度,然后给箭加一个力
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Shooting;
public class GameModel : MonoBehaviour {
public GameObject TargetItem, ArrowItem;
private GameObject holdingArrow = null, target;
private const int ARROW_SPEED = 30;
private SceneController scene;
void Awake() {
ArrowFactory.getInstance().initItem(ArrowItem);
}
void Start () {
scene = SceneController.getInstance();
scene.setGameModel(this);
target = Instantiate(TargetItem);
}
void Update () {
ArrowFactory.getInstance().detectReuseArrows();
}
//检测是否握住箭
public bool haveArrowOnPort() {
return (holdingArrow != null);
}
//产生箭
public void createArrow() {
if (holdingArrow == null) {
holdingArrow = ArrowFactory.getInstance().getArrow();
}
}
//使箭跟着鼠标移动
public void arrowFollowMouse(Vector3 mousePos) {
holdingArrow.transform.LookAt(mousePos * 30);
}
//射箭
public void shootTheArrow(Vector3 mousePos) {
holdingArrow.transform.LookAt(mousePos * 30);
holdingArrow.GetComponent<Rigidbody>().isKinematic = false;
//加风力
addWindForce();
holdingArrow.GetComponent<Rigidbody>().AddForce(mousePos * 30, ForceMode.Impulse);
holdingArrow = null;
}
void addWindForce() {
int windDir = scene.getWindDir();
int windStrength = scene.getWindStrength();
Vector3 windForce;
switch (windDir) {
case 0:
windForce = new Vector3(0, 1, 0);
break;
case 1:
windForce = new Vector3(1, 1, 0);
break;
case 2:
windForce = new Vector3(1, 0, 0);
break;
case 3:
windForce = new Vector3(1, -1, 0);
break;
case 4:
windForce = new Vector3(0, -1, 0);
break;
case 5:
windForce = new Vector3(-1, -1, 0);
break;
case 6:
windForce = new Vector3(-1, 0, 0);
break;
case 7:
windForce = new Vector3(-1, 1, 0);
break;
default:
windForce = Vector3.zero;
break;
}
holdingArrow.GetComponent<Rigidbody>().AddForce(windForce * windStrength * 20, ForceMode.Force);
}
}
3、ArrowFactory.cs
单例箭工厂。同样,设置一个usingList和unusedList,进行箭的循环利用。由GameModel的Update()方法让箭工厂每帧检测回收掉地的箭。箭掉地后,复位,同时要设置为运动学刚体。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Com.Shooting {
public class ArrowFactory : System.Object {
private static ArrowFactory instance;
private List<GameObject> usingArrowList = new List<GameObject>();
private List<GameObject> unusedArrowList = new List<GameObject>();
private GameObject ArrowItem;
private Vector3 ARROW_INIT_POS = new Vector3(0, 0, -8);
public static ArrowFactory getInstance() {
if (instance == null)
instance = new ArrowFactory();
return instance;
}
public void initItem(GameObject _ArrowItem) {
ArrowItem = _ArrowItem;
}
//提供箭
public GameObject getArrow() {
if (unusedArrowList.Count == 0) { //没有存储箭
GameObject newArrow = Camera.Instantiate(ArrowItem);
usingArrowList.Add(newArrow);
return newArrow;
}
else { //有存储箭
GameObject oldArrow = unusedArrowList[0];
unusedArrowList.RemoveAt(0);
oldArrow.SetActive(true);
usingArrowList.Add(oldArrow);
return oldArrow;
}
}
//检测箭落地,回收。此方法由GameModel的update()方法触发
public void detectReuseArrows() {
for (int i = 0; i < usingArrowList.Count; i++) {
if (usingArrowList[i].transform.position.y <= -8) {
usingArrowList[i].GetComponent<Rigidbody>().isKinematic = true;
usingArrowList[i].SetActive(false);
usingArrowList[i].transform.position = ARROW_INIT_POS;
unusedArrowList.Add(usingArrowList[i]);
usingArrowList.Remove(usingArrowList[i]);
i--;
SceneController.getInstance().modifyWind();
}
}
}
}
}
4、GameStatus.cs
游戏状态类,管理分数、风力状态等。每一次射箭后都会随机改变风力状态。射中靶后加分,并提示环数。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Shooting;
using UnityEngine.UI;
public class GameStatus : MonoBehaviour {
public GameObject canvasItem, scoreTextItem, TipsTextItem, windTextItem;
private int score = 0;
private int windDir = 0;
private int windStrength = 0;
private string[] windTextSet;
private const float TIPS_TEXT_SHOW_TIME = 0.6f;
private GameObject canvas, scoreText, TipsText, windText;
private SceneController scene;
void Start () {
scene = SceneController.getInstance();
scene.setGameStatus(this);
canvas = Instantiate(canvasItem);
scoreText = Instantiate(scoreTextItem, canvas.transform);
scoreText.transform.Translate(canvas.transform.position);
scoreText.GetComponent<Text>().text = "Score: " + score;
TipsText = Instantiate(TipsTextItem, canvas.transform);
TipsText.transform.Translate(canvas.transform.position);
TipsText.SetActive(false);
windTextSet = new string[8] { "↑", "↗", "→", "↘", "↓", "↙", "←", "↖" };
windText = Instantiate(windTextItem, canvas.transform);
windText.transform.Translate(canvas.transform.position);
modifyWind();
}
void Update () {
}
//得分,+10
public void addScore(int point) {
score += point;
scoreText.GetComponent<Text>().text = "Score: " + score;
modifyWind();
}
//提示环数
public void showTipsText(int point) {
TipsText.GetComponent<Text>().text = point + " points!";
TipsText.SetActive(true);
StartCoroutine(waitForSomeAndDisappearTipsText());
}
//延时消失
IEnumerator waitForSomeAndDisappearTipsText() {
yield return new WaitForSeconds(TIPS_TEXT_SHOW_TIME);
TipsText.SetActive(false);
}
//调整风向、强度
public void modifyWind() {
windDir = Random.Range(0, 8);
windStrength = Random.Range(0, 8);
windText.GetComponent<Text>().text = "Wind: " + windTextSet[windDir] + " x" + windStrength;
}
public int getWindDir() {
return windDir;
}
public int getWindStrength() {
return windStrength;
}
}
5、UserInterface.cs
用户操作类,检测用户按空格键(取箭)、按住鼠标左键移动(使箭跟着移动)、松开左键(射箭)三种行为。通过场景控制器SceneController的接口方法做出相关逻辑实现:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Shooting;
public class UserInterface : MonoBehaviour {
private IUserAction action;
private IGameStatusOp gameStatusOp;
void Start () {
action = SceneController.getInstance() as IUserAction;
gameStatusOp = SceneController.getInstance() as IGameStatusOp;
}
void Update () {
detectMouseInput();
detectKeyInput();
}
void detectMouseInput() {
if (gameStatusOp.haveArrowOnPort()) {
if (Input.GetMouseButton(0)) { //按住鼠标,箭跟鼠标移动
Vector3 mousePos = Camera.main.ScreenPointToRay(Input.mousePosition).direction;
action.arrowFollowMouse(mousePos);
}
if (Input.GetMouseButtonUp(0)) { //松开鼠标,发射
Vector3 mousePos = Camera.main.ScreenPointToRay(Input.mousePosition).direction;
action.shootTheArrow(mousePos);
}
}
}
void detectKeyInput() {
if (Input.GetKeyDown(KeyCode.Space)) { //按下空格键,产生箭
action.createArrow();
}
}
}
6、ArrowCollider.cs
这应该是这次作业最重要的一个类了(虽然代码不多)。
(1)此类挂载在箭头上。(注意不能挂载到整个箭组合上,因为:当箭头从靶上方飞过,但刚好箭身触碰到靶时,也会触发碰撞Enter方法,使箭变为运动学刚体而静止,从而造成奇异的效果)
(2)由于上面设置了箭头Is Trigger为true,即不会产生碰撞效果,所以永远不会触发OnCollisionEnter方法。因此,需要将碰撞进入方法改为void OnTriggerEnter(Collider c)。
(3)为什么需要设置箭头为inactive:这样设置是为了防止多次触发OnTriggerEnter方法
(4)由于触碰后整个箭组合会变为运动学刚体而静止,所以箭身会插在靶上
(5)根据射中的内靶确定环数
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Com.Shooting;
/*********************************************
* 此脚本加在箭头上
*********************************************/
public class ArrowCollider : MonoBehaviour {
private IGameStatusOp gameStatusOp;
void Start () {
gameStatusOp = SceneController.getInstance() as IGameStatusOp;
}
void Update () {
}
void OnTriggerEnter(Collider c) {
if (c.gameObject.tag == "target") {
//Debug.Log(this.gameObject.name + ": " + c.gameObject.name);
gameObject.transform.parent.gameObject.GetComponent<Rigidbody>().isKinematic = true;
gameObject.SetActive(false); //这样设置是为了防止多次触发
int point = c.gameObject.name[c.gameObject.name.Length - 1] - '0'; //得分
gameStatusOp.showTipsText(point);
gameStatusOp.addScore(point * 10);
}
}
}
操作步骤:
1、所有的预设如下:
(1)画板啊文字啊那些按照上次作业那样设置就好了。
(2)箭组合的设计上面提到了
(3)靶那加了个小Camera,就像效果图那右上角的小图,能够看得更清楚箭射中的情况。
2、上述6个脚本,UserInterface.cs挂载到主摄像机上;创建一个空对象Empty,挂载GameModel.cs、GameStatus.cs两个脚本
那就应该差不多了~~~