这次作业会在后面放完整代码和操作步骤的[破涕为笑]
进入正题之前先解释下几个代码里用到的几个英文单词吧_(:зゝ∠)_:
飞碟:dart 发射:launch 打击:strike 击中:struck
游戏规则rules:
按空格键发射飞碟,鼠标点击打飞碟。第n关发射n个飞碟,击中1个飞碟得100分,击不中(飞碟落地)1个扣100分。第n关需要打够n*100分才能进下一关(如:第4关需要打够400分)。
下面po一下效果图:
UML类图:
先大概解释一下UML图吧(下面还会详细解释):
1、MainSceneController:单例控制类,实现IUserAction接口
2、UserInterface:检测用户操作(键盘&鼠标),调用IUserAction接口方法触发发射飞镖和击打飞镖行为
3、DartFactory:单例飞碟工厂,负责提供飞碟、检测飞碟着地、回收飞碟
4、GameModel:获取飞碟并发射;检测是否击中飞碟
5、GameStatus:管理所有游戏状态,如round数、score得分、提示文字等
代码解释:
1、MainSceneController.cs:
单例控制类。有两个子对象GameModel和GameStatus。实现IUserAction接口(提供给用户UserInterface使用)的两个方法:发射飞碟和根据鼠标位置检测是否击中飞碟,实现方式为调用子对象GameModel的两个同名的方法。另外也实现IGameStatusOp接口(此接口供游戏扩展功能使用,这次没有真正用到,仅由GameModel调用了一下)的三个方法:获取round数、得分、扣分,实现方式也为调用子对象GameStatus的三个同名的方法。
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace PlayDarts.Com {
public interface IUserAction {
void launchDarts();
void strikeTheDart(Vector3 mousePos);
}
public interface IGameStatusOp {
int getRoundNum();
void addScore();
void subScore();
}
public class MainSceneController : System.Object, IUserAction, IGameStatusOp {
private static MainSceneController instance;
private GameModel myGameModel;
private GameStatus myGameStatus;
public static MainSceneController getInstance() {
if (instance == null)
instance = new MainSceneController();
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 launchDarts() {
myGameModel.launchDarts();
}
public void strikeTheDart(Vector3 mousePos) {
myGameModel.strikeTheDart(mousePos);
}
/**
* 实现IGameStatusOp接口
*/
public int getRoundNum() {
return myGameStatus.getRoundNum();
}
public void addScore() {
myGameStatus.addScore();
}
public void subScore() {
myGameStatus.subScore();
}
}
}
2、UserInterface.cs:
在Update()方法里检测用户操作(键盘&鼠标),调用IUserAction接口方法触发发射飞镖和击打飞镖行为
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using PlayDarts.Com;
public class UserInterface : MonoBehaviour {
private IUserAction action;
void Start () {
action = MainSceneController.getInstance() as IUserAction;
}
void Update () {
detectSpaceKeyAndLaunchDarts();
detectMouseDownAndStrikeTheDart();
}
void detectSpaceKeyAndLaunchDarts() {
if (Input.GetKeyDown(KeyCode.Space)) {
action.launchDarts();
}
}
void detectMouseDownAndStrikeTheDart() {
if (Input.GetMouseButtonDown(0)) {
Vector3 mouseWorldPosition = Input.mousePosition;
action.strikeTheDart(mouseWorldPosition);
}
}
}
3、DartFactoryBC.cs:
(正在发射)的飞镖,另一个存储没在使用的飞镖(或者落地或者被击中后回收)。这样设置目的在于,当存在没使用的飞镖时,可以重复使用,而不是新创建,从而节省资源!这里的检测落地由GameModel的Update()方法触发。
velocity= Vector3.zero;
PPS:还有注意代码里在单例类里面初始化飞碟模板的方法,即需要一个继承MonoBehaviour并挂载到Main Camera的类先初始化:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using PlayDarts.Com;
namespace PlayDarts.Com {
public class DartFactory : System.Object {
private static DartFactory instance;
private List<GameObject> usingDartList = new List<GameObject>(); //正在使用的飞镖list
private List<GameObject> unusedDartList = new List<GameObject>(); //没有使用的飞镖list
private GameObject dartItem;
public static DartFactory getInstance() {
if (instance == null)
instance = new DartFactory();
return instance;
}
//提供飞镖
public GameObject getDart() {
if (unusedDartList.Count == 0) { //没有存储飞镖
GameObject newDart = Camera.Instantiate(dartItem);
usingDartList.Add(newDart);
return newDart;
}
else { //有存储飞镖
GameObject oldDart = unusedDartList[0];
unusedDartList.RemoveAt(0);
oldDart.SetActive(true);
usingDartList.Add(oldDart);
return oldDart;
}
}
//update()检测飞镖落地,回收。此方法由GameModel的update()方法触发
public void detectReuseDarts() {
for (int i = 0; i < usingDartList.Count; i++) {
if (usingDartList[i].transform.position.y <= -8) {
usingDartList[i].GetComponent<Rigidbody>().velocity = Vector3.zero; //很重要
usingDartList[i].SetActive(false);
unusedDartList.Add(usingDartList[i]);
usingDartList.Remove(usingDartList[i]);
i--;
MainSceneController.getInstance().subScore(); //打不中,扣分
}
}
}
//飞镖被击中,回收
public void ReuseWhenDartBeingStruck(GameObject StruckDart) {
StruckDart.GetComponent<Rigidbody>().velocity = Vector3.zero; //很重要
StruckDart.SetActive(false);
unusedDartList.Add(StruckDart);
usingDartList.Remove(StruckDart);
}
//告知是否正在发射飞镖:若是则不能重复发射
public bool isLaunching() {
return (usingDartList.Count > 0);
}
//初始化dartItem
public void initItems(GameObject _dartItem) {
dartItem = _dartItem;
}
}
}
public class DartFactoryBC : MonoBehaviour {
public GameObject dartItem;
void Awake() {
DartFactory.getInstance().initItems(dartItem);
}
}
4、GameModel.cs:
MainSceneController的子对象,首先负责生成游戏场景需要的GameObject,比如地面、飞碟发射器、爆炸粒子。有两个主要方法是:获取飞碟并发射;根据鼠标位置检测是否击中飞碟。
Coroutine来实现延时错开发射(Coroutine是个特别好用的东西,有时候做多线程游戏开发时可以做线程用,建议自己网上学习一下)。另外,对于飞碟的轨迹我没有用数学方法去算啊_(:зゝ∠)_,我是先给飞碟加了rigidbody刚体,即加了重力,发射的时候直接加一个带方向的Impulse的冲力就好了(具体见下面代码)
(2) 击打飞碟:根据课上说的射线就好了。击中加分并回收飞碟,同时释放爆炸粒子效果。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using PlayDarts.Com;
public class GameModel : MonoBehaviour {
public GameObject PlaneItem, LauncherItem, ExplosionItem;
public Material greenMat, redMat, blueMat;
private GameObject plane, launcher, explosion;
private MainSceneController scene;
private const float LAUNCH_GAP = 0.1f;
void Start () {
scene = MainSceneController.getInstance();
scene.setGameModel(this);
plane = Instantiate(PlaneItem);
launcher = Instantiate(LauncherItem);
explosion = Instantiate(ExplosionItem);
}
void Update () {
DartFactory.getInstance().detectReuseDarts();
}
//发射飞镖
public void launchDarts() {
int roundNum = scene.getRoundNum();
if (!DartFactory.getInstance().isLaunching())
StartCoroutine(launchDartsWithGapTime(roundNum));
}
//每个飞镖发射之间相差一段时间
IEnumerator launchDartsWithGapTime(int roundNum) {
for (int i = 0; i < roundNum; i++) {
GameObject dart = DartFactory.getInstance().getDart();
dart.transform.position = launcher.transform.position;
dart.GetComponent<MeshRenderer>().material = getMaterial(roundNum);
Vector3 force = getRandomForce();
dart.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
yield return new WaitForSeconds(LAUNCH_GAP);
}
}
Vector3 getRandomForce() {
int x = Random.Range(-30, 31);
int y = Random.Range(30, 41);
int z = Random.Range(20, 31);
return new Vector3(x, y, z);
}
//击打飞镖
public void strikeTheDart(Vector3 mousePos) {
Ray ray = Camera.main.ScreenPointToRay(mousePos);
RaycastHit hit;
if (Physics.Raycast(ray, out hit)) {
if (hit.collider.gameObject.tag.Equals("Dart")) {
createExplosion(hit.collider.gameObject.transform.position);
scene.addScore();
DartFactory.getInstance().ReuseWhenDartBeingStruck(hit.collider.gameObject);
}
}
}
void createExplosion(Vector3 position) {
explosion.transform.position = position;
explosion.GetComponent<ParticleSystem>().GetComponent<Renderer>().material =
getMaterial(scene.getRoundNum());
explosion.GetComponent<ParticleSystem>().Play();
}
Material getMaterial(int roundNum) {
switch (roundNum % 3) {
case 0:
return redMat;
case 1:
return greenMat;
case 2:
return blueMat;
default:
return redMat;
}
}
}
5、GameStatus.cs:
MainSceneController的子对象,同样需要先生成Round数、score数、提示文字。负责管理所有游戏状态,如round数、score得分、提示文字等。加分后都会判断是否满分,如果满分则改变Round数、score数,提示Round x。
提示Round x会一段时间后消失,也是使用了上面提到的Coroutine。即延迟一段时间后,使文字消失。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using PlayDarts.Com;
using UnityEngine.UI;
public class GameStatus : MonoBehaviour {
public GameObject canvasItem, roundTextItem, scoreTextItem, TipsTextItem;
private int roundNum = 1;
private int score = 0;
private const float TIPS_TEXT_SHOW_TIME = 0.8f;
private GameObject canvas, roundText, scoreText, TipsText;
private MainSceneController scene;
void Start () {
scene = MainSceneController.getInstance();
scene.setGameStatus(this);
canvas = Instantiate(canvasItem);
roundText = Instantiate(roundTextItem, canvas.transform);
roundText.transform.Translate(canvas.transform.position);
roundText.GetComponent<Text>().text = "Round: " + roundNum;
scoreText = Instantiate(scoreTextItem, canvas.transform);
scoreText.transform.Translate(canvas.transform.position);
scoreText.GetComponent<Text>().text = "Score: " + score + " / " + (roundNum * 100);
TipsText = Instantiate(TipsTextItem, canvas.transform);
TipsText.transform.Translate(canvas.transform.position);
showTipsText();
}
void Update () {
}
public int getRoundNum() {
return roundNum;
}
void addRoundNum() {
roundNum++;
roundText.GetComponent<Text>().text = "Round: " + roundNum;
}
public int getScore() {
return score;
}
//得分,+100
public void addScore() {
score += 100;
scoreText.GetComponent<Text>().text = "Score: " + score + " / " + (roundNum * 100);
checkScore();
}
//扣分,-100
public void subScore() {
score = score >= 100 ? score - 100 : 0;
scoreText.GetComponent<Text>().text = "Score: " + score + " / " + (roundNum * 100);
}
//检测分数是否已满
void checkScore() {
if (score >= roundNum * 100) { //可以下一关
addRoundNum();
resetScore();
showTipsText();
}
}
void resetScore() {
score = 0;
scoreText.GetComponent<Text>().text = "Score: " + score + " / " + (roundNum * 100);
}
void showTipsText() {
TipsText.GetComponent<Text>().text = "Round " + roundNum + " !";
TipsText.SetActive(true);
StartCoroutine(waitForSomeAndDisappearTipsText());
}
IEnumerator waitForSomeAndDisappearTipsText() {
yield return new WaitForSeconds(TIPS_TEXT_SHOW_TIME);
TipsText.SetActive(false);
}
}
操作方法:
1、先在Assets文件夹下创建一个Materials文件夹,装材质Material。创建红、绿、蓝、黄4种颜色的材质(用在飞碟和发射器上)
2、Assets文件夹下创建Resources文件夹,里面创建Prefabs文件夹,装预设prefab。创建以下预设:
Prefabs文件夹。
(2) 地板MyPlane:Create->3D Object->Plane。然后修改成以下属性,最后拖到Prefabs文件夹。
(3) 发射器launcher:Create->3D Object->Cylinder。然后修改成以下属性:注意添加刚在Materials文件夹里创建的黄色材质。最后拖到Prefabs文件夹。
(4) 画板Canvas:Create->UI->Canvas。拖下来
(5) 回合文字Round Text、得分文字Score Text、提示文字Tips Text:Create->UI->Text。修改,然后拖下来。
(6) 爆炸粒子效果Explosion:Create->Particle System。修改,拖下来:
预设终于弄完_(:зゝ∠)_
3、Assets下创建Scripts文件夹,装代码文件。然后单击每个cs文件,看右上角,把预设啊、材料啊那些放进去:
(注意一定要放完整哦!)
4、最后就是把脚本啊那些拖到合适位置了:创建一个空对象Empty。UserInterface.cs挂载到Main Camera上;DartFactoryBC.cs、GameModel.cs、GameStatus.cs挂载到Empty上
5、最后还有很重要的一个步骤哦!因为现在的效果重力下降很缓慢,不真实。所以我们要修改重力加速度,使飞碟发射效果更真实!步骤如下:菜单栏Edit->Project Settings->Physics。Y那里原来是-9.81改为-20 就好啦。
终于搞定!那么开始游戏吧!!!