首先制作所需要的游戏对象。
创建一个Empty对象,命名为Target,然后创建五个Cylinder子对象,分别代表五个环,为了视觉效果好看些可以给环上不同的颜色,只需自己制作想要颜色的Material即可。五个环的属性如下:
注意最后这里我添加了第二个摄像机,用来近距离观察靶对象,必须设置clear flags属性为Depth Only,每次Render之前,只是清空深度缓冲区,但是并没有清空颜色缓冲区。这样第二个摄像机的画面就能够出现在主摄像机当中。
创建一个Empty对象为Arrow,其子对象使用一个Cylinder作为箭体,一个Cube作为箭头。箭头Box Collider的Is Trigger打勾,然后给Arrow添加rigidbody。
这里为箭头添加了碰撞监听事件,因为当弓箭击中靶目标的时候会插在靶上,并且根据击中的环得分,在箭头添加监听事件可以获取到碰撞到的是哪一个环,同时能够控制弓箭停在靶上。
其他游戏对象的制作可在源码中查看。
在实现场记之前,先定义一下游戏所需要的接口。用户可以获取弓箭,移动弓箭的位置以及射出弓箭。场记管理的动作有增加或减少得分,改变风向及强度,同时也暴露出获取风向,风的强度大小的接口以供UI显示。
public interface IUserAction
{
void getArrow();
void moveArrow();
void shootArrow();
}
public interface ISceneController
{
void addScore(int point);
void showTips(int point);
void changeWind();
int getWindDirection();
int getWindStrength();
bool ifReadyToShoot();
}
下面是场记的实现:
public class SceneController : System.Object, IUserAction, ISceneController
{
private static SceneController instance;
private GameStateController gameStateController;
private ArrowController arrowController;
public static SceneController getInstance()
{
if (instance == null) instance = new SceneController();
return instance;
}
public void setGameStatueController(GameStateController gsc)
{
gameStateController = gsc;
}
public void setArrowController(ArrowController _arrowController)
{
arrowController = _arrowController;
}
public void addScore(int point)
{
gameStateController.addScore(point);
}
public void changeWind()
{
gameStateController.changeWind();
}
public void getArrow()
{
arrowController.getArrow();
}
public int getWindDirection()
{
return gameStateController.getWindDirec();
}
public int getWindStrength()
{
return gameStateController.getWindStrength();
}
public bool ifReadyToShoot()
{
return arrowController.ifReadyToShoot();
}
public void moveArrow(Vector3 mousePos)
{
arrowController.moveArrow(mousePos);
}
public void shootArrow(Vector3 mousePos)
{
arrowController.shootArrow(mousePos);
}
public void showTips(int point)
{
gameStateController.showTips(point);
}
}
场记请来了游戏状态记录员(GameStateController)及弓箭管理者(ArrowController)来协助它管理这个游戏。状态记录员和弓箭管理者均为单实例对象。接下来是他们的实现。
状态管理员持有得分,风向,提示等文本对象的预制,然后在开始的时候实例化它们,并且实现场记交给它的任务。
public class GameStateController : MonoBehaviour {
public GameObject canvasPrefabs, scoreTextPrefabs, tipsTextPrefabs, windTextPrefabs;
private int score = 0, windDir = 0, windStrength = 0;
private const float TIPS_SHOW_TIME = 0.5f;
private GameObject canvas, scoreText, tipsText, windText;
private SceneController scene;
private string[] windDirectionArray;
// Use this for initialization
void Start () {
scene = SceneController.getInstance();
scene.setGameStatueController(this);
canvas = Instantiate(canvasPrefabs);
scoreText = Instantiate(scoreTextPrefabs);
tipsText = Instantiate(tipsTextPrefabs);
windText = Instantiate(windTextPrefabs);
scoreText.GetComponent().text = "Score: " + score;
tipsText.SetActive(false);
windDirectionArray = new string[8] { "↑", "↗", "→", "↘", "↓", "↙", "←", "↖" };
changeWind();
}
public void changeWind()
{
windDir = UnityEngine.Random.Range(0,8);
windStrength = UnityEngine.Random.Range(0, 8);
windText.GetComponent().text = "Wind: " + windDirectionArray[windDir] + " x" + windStrength;
}
internal void addScore(int point)
{
score += point;
scoreText.GetComponent().text = "Score: " + score;
changeWind();
}
// Update is called once per frame
void Update () {
}
internal int getWindDirec()
{
return windDir;
}
internal int getWindStrength()
{
return windStrength;
}
//提示命中环数
internal void showTips(int point)
{
tipsText.GetComponent().text = point + " Points!";
tipsText.SetActive(true);
StartCoroutine(waitForTipsDisappear());
}
private IEnumerator waitForTipsDisappear()
{
yield return new WaitForSeconds(TIPS_SHOW_TIME);
tipsText.SetActive(false);
}
}
由于箭在飞的过程中会受到风力的影响,当弓箭管理者射出弓箭的时候应该给箭加上一个风力,这里我用一组向量数组保存风力的8个风向,可以根据风向直接得到对应的向量,然后乘以风力大小便可以很容易地得到一个风力。
public class ArrowController : MonoBehaviour {
public GameObject TargetPrefabs, ArrowPrefabs;
private GameObject holdingArrow, target;
private const int SPEED = 35;
private SceneController scene;
private Vector3[] winds = new Vector3[8]
{
new Vector3(0, 1, 0), new Vector3(1,1,0), new Vector3(1,0,0), new Vector3(1,-1,0), new Vector3(0,-1,0), new Vector3(-1,-1,0), new Vector3(-1,0,0), new Vector3(-1,1,0)
};
private void Awake()
{
ArrowFactory.getInstance().init(ArrowPrefabs);
}
// Use this for initialization
void Start () {
scene = SceneController.getInstance();
scene.setArrowController(this);
target = Instantiate(TargetPrefabs);
}
// Update is called once per frame
void Update () {
ArrowFactory.getInstance().detectArrowsReuse();
}
public bool ifReadyToShoot()
{
return (holdingArrow != null);
}
internal void getArrow()
{
if (holdingArrow == null) holdingArrow = ArrowFactory.getInstance().getArrow();
}
internal void moveArrow(Vector3 mousePos)
{
//箭头跟随鼠标移动
holdingArrow.transform.LookAt(mousePos * 30);
}
internal void shootArrow(Vector3 mousePos)
{
holdingArrow.transform.LookAt(mousePos * 30);
holdingArrow.GetComponent().isKinematic = false;
addWind(); //箭在射出过程中会持续受到一个风力
holdingArrow.GetComponent().AddForce(mousePos * 30, ForceMode.Impulse);
holdingArrow = null;
}
private void addWind()
{
int windDir = scene.getWindDirection();
int windStrength = scene.getWindStrength();
Vector3 windForce = winds[windDir];
holdingArrow.GetComponent().AddForce(windForce, ForceMode.Force);
}
}
弓箭工厂实现为单实例工厂,这么做的好处是当需要向工厂获取弓箭或者检测是否能回收弓箭的时候,访问到的都是同一个工厂对象,其维护的两个弓箭列表自然也是相同的,从而实现资源的回收和利用。
public class ArrowFactory : System.Object
{
private static ArrowFactory instance;
private GameObject ArrowPrefabs;
private List usingArrowList = new List();
private List freeArrowList = new List();
private Vector3 INITIAL_POS = new Vector3(0, 0, -19);
public static ArrowFactory getInstance()
{
if (instance == null) instance = new ArrowFactory();
return instance;
}
public void init(GameObject _arrowPrefabs)
{
ArrowPrefabs = _arrowPrefabs;
}
internal void detectArrowsReuse()
{
for(int i = 0; i < usingArrowList.Count; i++)
{
if(usingArrowList[i].transform.position.y <= -8)
{
usingArrowList[i].GetComponent().isKinematic = true;
usingArrowList[i].SetActive(false);
usingArrowList[i].transform.position = INITIAL_POS;
freeArrowList.Add(usingArrowList[i]);
usingArrowList.Remove(usingArrowList[i]);
i--;
SceneController.getInstance().changeWind();
}
}
}
internal GameObject getArrow()
{
if(freeArrowList.Count == 0)
{
GameObject newArrow = Camera.Instantiate(ArrowPrefabs);
usingArrowList.Add(newArrow);
return newArrow;
}
else
{
GameObject oldArrow = freeArrowList[0];
freeArrowList.RemoveAt(0);
oldArrow.SetActive(true);
usingArrowList.Add(oldArrow);
return oldArrow;
}
}
}
public class UserInterface : MonoBehaviour {
private IUserAction action;
private ISceneController scene;
// Use this for initialization
void Start () {
action = SceneController.getInstance() as IUserAction;
scene = SceneController.getInstance() as ISceneController;
}
// Update is called once per frame
void Update () {
//用户按下空格,获取弓箭
if (Input.GetKeyDown(KeyCode.Space)) action.getArrow();
if (scene.ifReadyToShoot())
{
//按住鼠标左键,跟随移动
if (Input.GetMouseButton(0))
{
Vector3 mousePos = Camera.main.ScreenPointToRay(Input.mousePosition).direction;
action.moveArrow(mousePos);
}
//左键松开,射出弓箭
if (Input.GetMouseButtonUp(0))
{
Vector3 mousePos = Camera.main.ScreenPointToRay(Input.mousePosition).direction;
action.shootArrow(mousePos);
}
}
}
}
在制作靶对象的预设的时候,我把五个环设了一个tag:target,这样当弓箭碰撞到靶的时候可以通过tag值判断是否命中靶,并把每个环以“T”+数字命名,这样名字后面的数字即表示命中该环的得分。
public class ArrowCollider : MonoBehaviour {
private ISceneController scene;
// Use this for initialization
void Start () {
scene = SceneController.getInstance() as ISceneController;
}
// Update is called once per frame
void Update () {
}
void OnTriggerEnter(Collider other)
{
if(other.gameObject.tag == "target")
{
gameObject.transform.parent.gameObject.GetComponent().isKinematic = true;
gameObject.SetActive(false);
int points = other.gameObject.name[other.gameObject.name.Length - 1] - '0';
scene.showTips(points);
scene.addScore(points);
}
}
}
演示视频:https://www.bilibili.com/video/av22529888/
完整代码:https://github.com/CarolSum/Unity3d-Learning/tree/master/hw5/TargetGame