简单的2D射击游戏实践

简单的2D射击游戏实践

  Unity3D编辑器虽然从名字上看是个制作3D游戏的工具,但其实它也提供了2D游戏制作的相关资源和功能,而且使用Unity3D来制作2D游戏并不难,只要准备好相关的图片等资源文件,有一定的编程能力就可以动手了。
  接下来的分享的一个小项目是作者刚刚学习Unity3D没多久的时候为了实践一些想法和巩固对Unity3D游戏结构与制作方法的印象而编写的,非常简单,也没有使用商店里的插件,编程的方式也是最基础的那种。


游戏未动,设计先行

  编写一个游戏不能上来就着手编写代码,这样是什么都做不出来的。对于从零开始制作一个游戏这种情况而言,最先要做的一件事情就是设计。
  设计的方式和过程那自然因人而异,可以用图形,也可以用文字,但目的都是一样的,通过某种介质大概记录下自己对游戏的想法,最后整理归纳出来便是设计的开始。
  就本文要分享的例子而言,设计的描述如下

  • 游戏主要展示方式为2D横版射击

    • 限定地图大小
    • 键盘控制移动
    • 鼠标控制射击
    • 镜头跟随角色
  • 一些游戏的重点描述

    • 简单的资源系统
    • 玩家和敌人都存在血量
    • 在完成一个关卡后可以在切换时消耗资源回复血量
    • 关卡难度随着关卡编号和玩家等级变化而变化

  简单的文字描述把游戏的大概设计确定下来,接着进行细分设计,鉴于Unity3D制作游戏时基本都以场景为单位进行,因此细分设计也直接按照场景分类

  • 主菜单场景
    • 该场景为启动游戏时进入的第一个场景
    • 标题文字
    • 开始游戏按钮
    • 退出游戏按钮
  • 游戏场景
    • 游戏主要的游玩场景
    • 地图展示
    • 角色控制
    • 敌人生成
    • 资源生成
    • 关卡出口
  • 关卡切换场景
    • 玩家角色在两个关卡之间暂歇的场景
    • 角色信息展示
    • 资源使用
    • 存档按钮
    • 继续按钮
    • 退出按钮

  有了细分之后就可以根据它来准备各种资源了,比如角色的图片资源,UI的背景资源等;在准备的同时可以画一些框图,将场景界面设计出来,方便制作时按图索骥。

  • 主菜单场景设计图

简单的2D射击游戏实践_第1张图片
  场景中只有一个UI界面,标题文字固定为游戏的标题,玩家在开始游戏之前需要在输入框里输入自己的昵称,退出游戏会关闭整个程序。

  • 游戏主场景设计图

简单的2D射击游戏实践_第2张图片
  其中主游戏窗口展示地图,玩家角色和敌人等物体,玩家信息主要展示玩家角色的头像,血量和经验值,资源信息则展示玩家角色在地图中收集到的资源情况。

  • 关卡切换场景设计图

简单的2D射击游戏实践_第3张图片
  角色信息展示头像和血量等,资源使用情况包含资源的数值和使用方法的功能按钮,过场标题为文字,按钮部分可以切换到下一关或者存档,也可以直接退出当前游戏。
  有了这些设计文字和框图,下一步就应该进入正式的设计阶段了,出于简单起见,概要设计文档就不整合了,直接进入UML用例图设计。

  • 开始菜单场景用例

简单的2D射击游戏实践_第4张图片

  • 游戏场景用例

简单的2D射击游戏实践_第5张图片

  • 切换场景用例

简单的2D射击游戏实践_第6张图片
  至此UML用例图初步设计完成。
  接下来进行功能细分,方便后续设计游戏的类图。
  所谓功能细分就是要进一步细化每个场景以及每个用例中包含的功能,并且细化到单个方法的层次,这样才方便后续的类图设计。
  鉴于用例众多,就不一一细分了,举例如下

  • 游戏场景需要游戏地图,因此在进入场景时需要生成游戏地图

    • 地图要有边界来限定玩家角色的运动
    • 地图要有障碍来妨碍玩家角色的躲避
    • 地图要有资源块来让玩家角色获取
  • 操作角色运动用例

    • 玩家角色对象要获取玩家输入操作
    • 玩家角色要根据输入操作进行不同的动作
    • 在没有操作时玩家角色要能回归静止

  诸如此类的细分,当全部功能都细分完成后就可以进入类图设计,将整个项目的代码结构大概表达出来。鉴于涉及到的类很多,在此也不费过多篇幅,举一个例子如下
  游戏中的地图是通过类似Tile-Based的区块地图生成方式随机生成的,因此需要一个地图相关的管理工具类。地图本身为18X18的正方形,每个格子为一个单位地块,单位地块可能是可通行的,也可能是障碍,其上可能有资源块,也可能有关卡出口。
  为了防止玩家角色跑出地图区域,需要为地图加上边界,考虑到通用性,边界使用和地图主体类似的地块,只是为它们添加不同的标签。
  类图如下
简单的2D射击游戏实践_第7张图片
  到此为止,设计阶段就算是差不多了,整理好文档后就可以进入下一步,也就是场景编辑和代码编写。


场景与代码

  场景编辑开始之前,先要根据前面的设计明确一些游戏机制。比如玩家角色的移动是什么样子的,玩家角色与障碍物的碰撞应该要达到什么样的效果,玩家发射的子弹应该是怎么飞行以及怎么爆炸的等等,明确了这些机制后,场景编辑才能顺利完成。
  还是以地图生成为例子,类图中设计了MapManager类,但是这个类是怎么完成地图的相关工作的?这个问题在动手之前需要有相对较为清晰的认识。
  既然设计中是采用Tile-Based的地块方式生成地图的,那么肯定需要一系列的地图块组件,类图中的GameObject成员以及List成员也能说明这一点,所以在场景中就需要好几个地块的预制体Prefab来方便使用。
  为了正确地实现碰撞,障碍物,敌人以及资源块等物体需要碰撞器,玩家角色也不例外,其中的一部分物体还需要刚体组件,这一点在预制体作成之前就要考虑好,避免中途有太多修改。
  生成地图的顺序是怎样的?是纯随机还是有一定算法?资源块的放置有没有限制条件?如何保证每一关都能有至少一条通关路线等等这些问题都会成为实际编码中要斟酌的东西。
  在本项目中的地块和资源块大概如下所示

  可通行的地块

简单的2D射击游戏实践_第8张图片

  障碍地块

简单的2D射击游戏实践_第9张图片

  资源块

简单的2D射击游戏实践_第10张图片
  其实它们之间并没有太大的不同,除了图片资源不同之外,主要就是标签不一样来方便碰撞处理识别对象类型。
  编辑好场景,接着就编写代码实现功能,为了方便地运行代码,首先在场景中添加一个名为MapManager的游戏对象,并挂载脚本MapManager。
简单的2D射击游戏实践_第11张图片
  而MapManger的具体代码如下所示

public class MapManager : MonoBehaviour {

    public GameManager gameManager;

    public GameObject edgeBlockLeft; // 左边界
    public GameObject edgeBlockTop; // 上边界
    public GameObject edgeBlockRight; // 右边界
    public GameObject edgeBlockBottom; // 下边界
    public GameObject edgeConerBlockLB; // 边界左下角
    public GameObject edgeConerBlockLT; // 边界左上角
    public GameObject edgeConerBlockRT; // 边界右上角
    public GameObject edgeConerBlockRB; // 边界右下角
    public GameObject[] oceanBlockList; // 可通行地块Prefab列表
    public GameObject[] islandBlockList; // 障碍地块Prefab列表
    public GameObject[] resourceBlockList; // 资源块Prefab列表
    public GameObject[] shipcoreBlockList; // 特殊资源块Prefab列表
    public GameObject factoryBlock; // 关卡出口对象

    public GameObject[] enemyShipList; // 敌人Prefab列表

    public GameObject mapObject; // 地图的根元素

    private List pointList; // 缓存可以放置物体的坐标

    private DataCache dataCache; // 动态数据

    private const int ROW_COUNT = 18; // 行数
    private const int COL_COUNT = 18; // 列数

    private const int ISLAND_COUNT = 24; // 障碍地块数量

    // Use this for initialization
    void Awake() {
        gameManager = this.GetComponent();
        dataCache = DataCache.getInstance();
        initMap();
    }

    // Update is called once per frame
    void Update() {

    }

    //初始化地图
    private void initMap() {
        pointList = new List();
        mapObject = new GameObject("Map");
        Transform mapHolder = mapObject.transform;
        // 准备创建地形
        TerrainGenerator(mapHolder);
        // 准备创建资源块(2-6个,含量数值与level挂钩)
        placeResourceBlocks(mapHolder);
        // 准备创建核心块(1-4个,纯随机)
        placeShipcoreBlocks(mapHolder);
        // 准备创建敌人(1-4个,攻防血与level挂钩)
        placeEnemyShips(mapHolder);
    }

    // 地形生成
    private void TerrainGenerator(Transform holder) {
        GameObject obj = null;
        int islandCount = 0;
        // 创建地形并缓存可用地块(即海洋地块)
        for (int x = 0; x < COL_COUNT; x++) {
            for (int y = 0; y < ROW_COUNT; y++) {
                Vector2 vec = new Vector2(x * 1.28f, y * 1.28f);
                int index = Random.Range(0, 8);
                if (x == 0) {
                    if (y == 0) {
                        obj = Instantiate(edgeConerBlockLB, vec, Quaternion.identity) as GameObject;
                    }
                    else if (y == ROW_COUNT - 1) {
                        obj = Instantiate(edgeConerBlockLT, vec, Quaternion.identity) as GameObject;
                    }
                    else {
                        obj = Instantiate(edgeBlockLeft, vec, Quaternion.identity) as GameObject;
                    }
                }
                else if (x == COL_COUNT - 1) {
                    if (y == 0) {
                        obj = Instantiate(edgeConerBlockRB, vec, Quaternion.identity) as GameObject;
                    }
                    else if (y == ROW_COUNT - 1) {
                        obj = Instantiate(edgeConerBlockRT, vec, Quaternion.identity) as GameObject;
                    }
                    else {
                        obj = Instantiate(edgeBlockRight, vec, Quaternion.identity) as GameObject;
                    }
                }
                else if (y == 0) {
                    obj = Instantiate(edgeBlockBottom, vec, Quaternion.identity) as GameObject;
                }
                else if (y == ROW_COUNT - 1) {
                    obj = Instantiate(edgeBlockTop, vec, Quaternion.identity) as GameObject;
                }
                else if (x > 1 && x < COL_COUNT - 2 && y > 1 && y < ROW_COUNT - 2) {
                    obj = Instantiate(oceanBlockList[index], vec, Quaternion.identity) as GameObject;
                    pointList.Add(vec);
                }
                else {
                    if (x == COL_COUNT - 2 && y == ROW_COUNT - 2) {
                        obj = Instantiate(oceanBlockList[index], vec, Quaternion.identity) as GameObject;
                        obj.transform.SetParent(holder);
                        obj = Instantiate(factoryBlock, vec, Quaternion.identity) as GameObject;
                    }
                    else {
                        obj = Instantiate(oceanBlockList[index], vec, Quaternion.identity) as GameObject;
                        pointList.Add(vec);
                    }
                }
                obj.transform.SetParent(holder);
            }
        }
        placeIslandBlocks(holder);
    }

    // 放置障碍
    private void placeIslandBlocks(Transform holder) {
        for(int i = 0; i < ISLAND_COUNT; i++) {
            Vector2 position = RandomPosition();
            GameObject islandObj = Instantiate(islandBlockList[Random.Range(0, 8)], position, Quaternion.identity) as GameObject;
            islandObj.transform.SetParent(holder);
        }
    }

    // 放置资源
    private void placeResourceBlocks(Transform holder) {
        int resCount = Random.Range(2, 7);
        for(int i = 0; i < resCount; i++) {
            Vector2 position = RandomPosition();
            GameObject resObj = Instantiate(resourceBlockList[i % 4], position, Quaternion.identity) as GameObject;
            resObj.transform.SetParent(holder);
        }
    }

    // 放置特殊资源
    private void placeShipcoreBlocks(Transform holder) {
        int coreCount = Random.Range(1, 4);
        for(int i = 0; i < coreCount; i++) {
            Vector2 position = RandomPosition();
            GameObject coreObj = Instantiate(shipcoreBlockList[i], position, Quaternion.identity) as GameObject;
            coreObj.transform.SetParent(holder);
        }
    }

    // 放置敌人
    private void placeEnemyShips(Transform holder) {
        int enemyCount = Random.Range(1, 5);
        for(int i = 0; i < enemyCount; i++) {
            Vector2 position = RandomPosition();
            GameObject enemyPrefab = RandomPrefab(enemyShipList, ((dataCache.Stage / 5) + 1));
            GameObject enemyObj = Instantiate(enemyPrefab, position, Quaternion.identity) as GameObject;
            enemyObj.transform.SetParent(holder);
        }
    }

    // 从缓存中随机取得一个坐标
    private Vector2 RandomPosition() {
        int randomIndex = Random.Range(0, pointList.Count);
        Vector2 resultVec = pointList[randomIndex];
        if (resultVec.x == 1.28f && resultVec.y == 1.28f) {
            pointList.RemoveAt(randomIndex);
            return RandomPosition();
        }
        return resultVec;
    }

    // 随机取得指定类型的Prefab
    private GameObject RandomPrefab(GameObject[] prefabs, int maxRange) {
        int randomIndex = Random.Range(0, maxRange);
        return prefabs[randomIndex];
    }
}

  至此关于地图的功能就编写完成了,其它部分的场景编辑与代码编写大同小异,在此就不长篇大论了,项目的完整代码会放在码云上分享。
  当然了,这个项目只是个非常简单的入门级Demo,和真正的游戏制作比起来差得远了,但它能很好地帮助理清一个游戏从设计到实现的流程与思路,就学习而言还是有一定作用的。

你可能感兴趣的:(Unity开发相关)