欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

3D游戏编程作业5

程序员文章站 2022-07-13 09:39:13
...

作业目标

编写一个简单的鼠标打飞碟(Hit UFO)游戏

  • 游戏内容要求:
    1. 游戏有 n 个 round,每个 round 都包括10 次 trial;
    2. 每个 trial 的飞碟的色彩、大小、发射位置、速度、角度、同时出现的个数都可能不同。它们由该 round 的 ruler 控制;
    3. 每个 trial 的飞碟有随机性,总体难度随 round 上升;
    4. 鼠标点中得分,得分规则按色彩、大小、速度不同计算,规则可*设定。
  • 游戏的要求:
    • 使用带缓存的工厂模式管理不同飞碟的生产与回收,该工厂必须是场景单实例的!具体实现见参考资源 Singleton 模板类
    • 近可能使用前面 MVC 结构实现人机交互与游戏模型分离

游戏规则

单击运行开始游戏:

  • 每次从第一回合开始,玩家按空格键“space”发射飞碟;
  • 玩家通过鼠标点击打飞碟,击中飞碟加十分;
  • 若飞碟掉落到地面还未被玩家击中,则扣除10分;
  • 当玩家分数达到回合数的20倍时,进入下一回合,分数累计到下一回合;
  • 随着回合数增加,飞碟数量增加,飞碟飞行速度变快,体积减小;
  • 当玩家分数低于0分时,游戏结束。

游戏设计

1、飞碟工厂

根据要求,我们要设计一个飞碟工厂来管理飞碟的产生和回收,这里要注意的是,我们回收的飞碟是可以再利用的,也就是说,当飞碟回收后,我们不应该把它销毁掉,而是应该改变它的状态,让它处于待使用的状态,所以我们建立了两个链:usingDisks和uselessDisks来分别存储这两种飞碟。

带缓存工厂模式的伪代码:

prepareDisks(diskCount)
BEGIN
	IF (free list has disk) THEN
	a_disk = remove one from list
	ELSE
	a_disk = clone from Prefabs
	ENDIF
	Set DiskData of a_disk with the diskCount
	Add a_disk to used list
	Return a_disk
END
destroyDisk(disk)
BEGIN
	Find disk in used list
	IF (not found) THEN THROW exception
	Move disk from used to free list
END

具体实现代码(位于DiskFactoryMono.cs中):

    public class DiskFactory : System.Object {
        public GameObject diskPrefab;
        private static DiskFactory diskFactory;
        List<GameObject> usingDisks;
        List<GameObject> uselessDisks;

        public static DiskFactory getFactory () {
            if (diskFactory == null) {
                diskFactory = new DiskFactory ();
                diskFactory.uselessDisks = new List<GameObject> ();
                diskFactory.usingDisks = new List<GameObject> ();
            }

            return diskFactory;
        }

        public List<GameObject> prepareDisks (int diskCount) {
            for (int i = 0; i < diskCount; i++) {
                if (uselessDisks.Count == 0) {
                    GameObject disk = GameObject.Instantiate<GameObject> (diskPrefab);
                    usingDisks.Add (disk);
                } else {
                    GameObject disk = uselessDisks[0];
                    uselessDisks.RemoveAt (0);
                    usingDisks.Add (disk);
                }
            }

            return this.usingDisks;
        }

        public void recycleDisk (GameObject disk) {
            int index = usingDisks.FindIndex (x => x == disk);
            uselessDisks.Add (disk);
            usingDisks.RemoveAt (index);
        }
    }

2、场景控制器( C )

根据自顶向下的思路考虑,要想设计一个MVC架构的游戏,首先我们需要考虑顶层的控制类场景控制器SceneController,这里我们为其设计调用了四种接口并在控制器中完成了对接口的实现,分别是IUserInterface(用户操作接口)、IQueryStatus(游戏状态查询接口)、setStatus(状态设置接口)和IScore(分数控制接口)。

public class SceneController : IUserInterface, IQueryStatus, setStatus, IScore {
        public int sendDiskCount { get; private set; }

        private static SceneController sceneController;
        private GameStatus gameStatus;
        private SceneStatus sceneStatus;
        private DiskFactory diskFactory = DiskFactory.getFactory ();
        public Scene scene;

        public static SceneController getGSController () {
            if (sceneController == null) {
                sceneController = new SceneController ();
            }

            return sceneController;
        }

        public void sendDisk () {
            int diskCount = scene.diskCount;
            var diskList = diskFactory.prepareDisks (diskCount);
            scene.sendDisk (diskList);
        }

        public void destroyDisk (GameObject disk) {
            scene.destroyDisk (disk);
            diskFactory.recycleDisk (disk);
        }

        public GameStatus queryGameStatus () {
            return gameStatus;
        }

        public SceneStatus querySceneStatus () {
            return sceneStatus;
        }

        public void setGameStatus (GameStatus gameStatus) {
            this.gameStatus = gameStatus;
        }

        public void setSceneStatus (SceneStatus sceneStatus) {
            this.sceneStatus = sceneStatus;
        }

        public void addScore () {
            ScoreRecorder.getScoreRecorder ().addScore ();
        }

        public void subScore () {
            ScoreRecorder.getScoreRecorder ().subScore ();
        }

        public int getScore () {
            return ScoreRecorder.getScoreRecorder ().score;
        }

        public void update () {
            scene.sceneUpdate ();
        }
    }

3、场景和界面( V )

接着考虑View层,主要是两部分:用户界面和场景设置。

场景设置(Scene)是用来设置飞碟的数量、大小、飞行速度等属性,控制飞碟的发射与回收,以及判断飞碟的状态并传输给控制类。

而用户界面则是提供人机交互的具体界面,包括四个部分:计分板、游戏状态栏、一个平面还有飞碟,其中飞碟只有在我们“召唤”时才会出现,游戏状态栏只有在游戏失败或成功时出现,这四个部分分别在UI.cs和UserInterface.cs文件中实现了。

Scene.cs:

public class Scene : MonoBehaviour
{
    public int round{get; set;}
    public int diskCount{get; private set;}

    private float diskScale;
    private float diskSpeed;
    private Color diskColor;
    private Vector3 startP;
    private Vector3 startD;

    List<GameObject> usingDisks;

    public void Reset(int round){
        this.round = round;
        this.diskCount = round;
        this.diskScale = 1;
        this.diskSpeed = 0.1f;

        if(round%2 == 1){
            this.diskColor = Color.red;
            this.startP = new Vector3(-5f, 3f, -15f);
            this.startD = new Vector3(3f, 8f, 5f);
        }
        else {
            this.diskColor = Color.green;
            this.startP = new Vector3(5f, 3f, -15f);
            this.startD = new Vector3(-3f, 8f, 5f);
        }

        for(int i=1 ; i<round ; i++){
            this.diskScale *= 0.8f;
            this.diskSpeed *= 1.1f;
        }
    }

    public void sendDisk(List<GameObject> usingDisks){
        this.usingDisks = usingDisks;
        this.Reset(round);

        for(int i=0 ; i<usingDisks.Count ; i++){
            var localScale = usingDisks[i].transform.localScale;

            usingDisks[i].transform.localScale = new Vector3(localScale.x*diskScale, localScale.y*diskScale, localScale.z*diskScale);
            usingDisks[i].GetComponent<Renderer>().material.color = diskColor;
            usingDisks[i].transform.position = new Vector3(startP.x, startP.y+i, startP.z);

            Rigidbody rigidbody;
            rigidbody = usingDisks[i].GetComponent<Rigidbody>();
            rigidbody.WakeUp();
            rigidbody.useGravity = true;
            rigidbody.AddForce(startD*Random.Range(diskSpeed*5, diskSpeed*8)/5, ForceMode.Impulse);
            SceneController.getGSController().setSceneStatus(SceneStatus.shooting);
        }
    }

    public void destroyDisk(GameObject disk){
        disk.GetComponent<Rigidbody>().Sleep();
        disk.GetComponent<Rigidbody>().useGravity = false;
        disk.transform.position = new Vector3(0f, -99f, 0f);
    }

    public void sceneUpdate(){
        round++;
        Reset(round);
    }

    private void Start(){
        this.round = 1;
        Reset(round);
    }

    private void Update(){
        if(usingDisks != null){
            for(int i=0 ; i<usingDisks.Count ; i++){
                if (usingDisks[i].transform.position.y <= 1){
                    SceneController.getGSController().destroyDisk(usingDisks[i]);
                    SceneController.getGSController().subScore();
                }
            }

            if(usingDisks.Count == 0){
                SceneController.getGSController().setSceneStatus(SceneStatus.waiting);
            }
        }
    }
}

UI.cs:
其实在设计中游戏状态栏一直存在于界面上,但是它是透明的,只有在有win或fail出现时才能看到。

public class UI : MonoBehaviour
{
    GameObject scoreText;
    GameObject gameStatusText;
    IScore score = SceneController.getGSController() as IScore;
    IQueryStatus status = SceneController.getGSController() as IQueryStatus;

    // Start is called before the first frame update
    void Start()
    {
        scoreText = GameObject.Find("Score");
        gameStatusText = GameObject.Find("GameStatus");
    }

    // Update is called once per frame
    void Update()
    {
        string scores = Convert.ToString(score.getScore());

        if(status.queryGameStatus() == GameStatus.fail){
            gameStatusText.GetComponent<Text>().text = "fail!";
        }
        else if(status.queryGameStatus() == GameStatus.win){
            gameStatusText.GetComponent<Text>().text = "win!";
        }

        scoreText.GetComponent<Text>().text = "Score: " + scores;
    }
}

UserInterface.cs:
这里设计为当我们按空格键(space)时,会在飞碟工厂中唤醒n个飞碟,将其发射出来;而当我们鼠标点击到飞碟时,飞碟会被销毁。

public class UserInterface : MonoBehaviour
{
    public GameObject planePrefab;

    GameStatus gameStatus;
    SceneStatus sceneStatus;

    IUserInterface userInterface;
    IQueryStatus queryStatus;
    IScore score;

    void Start(){
        GameObject plane = GameObject.Instantiate<GameObject>(planePrefab);
        plane.transform.position = new Vector3(0f, 0f, 70f);

        gameStatus = GameStatus.ongoing;
        sceneStatus = SceneStatus.waiting;

        userInterface = SceneController.getGSController() as IUserInterface;
        queryStatus = SceneController.getGSController() as IQueryStatus;
        score = SceneController.getGSController() as IScore;
    }

    void Update(){
        gameStatus = queryStatus.queryGameStatus ();
        sceneStatus = queryStatus.querySceneStatus ();

        if (gameStatus == GameStatus.ongoing) {
            if (sceneStatus == SceneStatus.waiting && Input.GetKeyDown ("space")) {
                userInterface.sendDisk ();
            }
            if (sceneStatus == SceneStatus.shooting && Input.GetMouseButtonDown (0)) {
                Ray ray = Camera.main.ScreenPointToRay (Input.mousePosition);
                RaycastHit hit;
                if (Physics.Raycast (ray, out hit) && hit.collider.gameObject.tag == "Disk") {
                    userInterface.destroyDisk (hit.collider.gameObject);
                    score.addScore ();
                }
            }
        }
    }
}

4、分数记录器( M )

此处的分数分数记录器是属于Model层,它负责游戏逻辑的设置,主要是加减分数,获得分数,判断游戏输赢以及判断是否进入下一回合这四个功能。

public class ScoreRecorder : IScore
{
    public int score{get; set;}
    public int round{get; set;}
    private static ScoreRecorder scoreRecorder;

    public static ScoreRecorder getScoreRecorder(){
        if(scoreRecorder == null){
            scoreRecorder = new ScoreRecorder();
            scoreRecorder.round = 1;
        }

        return scoreRecorder;
    }

    public void addScore(){
        score += 10;
        if(checkUpdate()){
            this.round++;
            SceneController.getGSController().update();
        }
    }

    public void subScore(){
        score -= 10;
        if(score <= 0){
            SceneController.getGSController().setGameStatus(GameStatus.fail);
        }
    }

    public bool checkUpdate(){
        if(score >= round*20)
            return true;
        return false;
    }

    public int getScore(){
        return ScoreRecorder.getScoreRecorder().score;
    }
}

同时游戏模型的预制也是属于Model层的,此次预制包括一个平面(Plane)和刚体飞碟(Disk)。
3D游戏编程作业5

游戏运行

将Script中的DiskFactoryMono.cs(内含DiskFactory)、GameMain.cs(内含SceneController)和UserInterface.cs(内含UserInterface)挂载到主摄像机上,并将预制的内容添加到对应位置;

然后创建一个UI游戏对象,其下包含两个Text游戏对象,分别命名为Score和GameStatus,放置在场景中对应位置,将UI.cs挂载到UI上;

点击运行,便可以进入游戏界面了。

3D游戏编程作业5

效果展示

3D游戏编程作业5

代码地址

传送门