这次作业会在后面放完整代码和操作步骤的[破涕为笑]


进入正题之前先解释下几个代码里用到的几个英文单词吧_(:зゝ∠)_:

飞碟:dart   发射:launch   打击:strike   击中:struck


游戏规则rules

按空格键发射飞碟,鼠标点击打飞碟。第n关发射n个飞碟,击中1个飞碟得100分,击不中(飞碟落地)1个扣100分。第n关需要打够n*100分才能进下一关(如:第4关需要打够400分)。

下面po一下效果图:

unity 答题为什么只能判断一次_System


UML类图:

unity 答题为什么只能判断一次_unity3d_02

    先大概解释一下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种颜色的材质(用在飞碟和发射器上)

unity 答题为什么只能判断一次_System_03

  

unity 答题为什么只能判断一次_unity 答题为什么只能判断一次_04

2、Assets文件夹下创建Resources文件夹,里面创建Prefabs文件夹,装预设prefab。创建以下预设:

unity 答题为什么只能判断一次_unity3d_05

Prefabs文件夹。

unity 答题为什么只能判断一次_打飞碟_06

(2) 地板MyPlane:Create->3D Object->Plane。然后修改成以下属性,最后拖到Prefabs文件夹。

unity 答题为什么只能判断一次_unity 答题为什么只能判断一次_07

(3) 发射器launcher:Create->3D Object->Cylinder。然后修改成以下属性:注意添加刚在Materials文件夹里创建的黄色材质。最后拖到Prefabs文件夹。

unity 答题为什么只能判断一次_unity 答题为什么只能判断一次_08

(4) 画板Canvas:Create->UI->Canvas。拖下来

(5) 回合文字Round Text、得分文字Score Text、提示文字Tips Text:Create->UI->Text。修改,然后拖下来。

unity 答题为什么只能判断一次_打飞碟_09

 

unity 答题为什么只能判断一次_unity3d_10

  

unity 答题为什么只能判断一次_打飞碟_11

(6) 爆炸粒子效果Explosion:Create->Particle System。修改,拖下来:

unity 答题为什么只能判断一次_unity3d_12

    预设终于弄完_(:зゝ∠)_

3、Assets下创建Scripts文件夹,装代码文件。然后单击每个cs文件,看右上角,把预设啊、材料啊那些放进去:

unity 答题为什么只能判断一次_unity3d_13

 

unity 答题为什么只能判断一次_unity 答题为什么只能判断一次_14

  

unity 答题为什么只能判断一次_打飞碟_15

  

unity 答题为什么只能判断一次_u3d课堂作业_16

(注意一定要放完整哦!)

4、最后就是把脚本啊那些拖到合适位置了:创建一个空对象Empty。UserInterface.cs挂载到Main Camera上;DartFactoryBC.cs、GameModel.cs、GameStatus.cs挂载到Empty上

unity 答题为什么只能判断一次_unity 答题为什么只能判断一次_17

  

unity 答题为什么只能判断一次_System_18

  

unity 答题为什么只能判断一次_System_19

5、最后还有很重要的一个步骤哦!因为现在的效果重力下降很缓慢,不真实。所以我们要修改重力加速度,使飞碟发射效果更真实!步骤如下:菜单栏Edit->Project Settings->Physics。Y那里原来是-9.81改为-20 就好啦。

unity 答题为什么只能判断一次_System_20


终于搞定!那么开始游戏吧!!!