unity3d游戏_unity3d游戏开发 - CSDN
精华内容
参与话题
  • Unity3D游戏开发——塔防小游戏

    万次阅读 2017-08-07 11:22:35
    游戏效果图: 本文参照siki学院的塔防游戏教程写的。http://www.sikiedu.com,搜索塔防游戏登入即可观看。 游戏流程: 1.首先创建cube调整其大小(以便计算),然后将其设成预设体(以便之后统一操作),利用...

    游戏效果图:


    本文参照siki学院的塔防游戏教程写的。http://www.sikiedu.com,搜索塔防游戏登入即可观看。

    游戏流程:

    1.首先创建cube调整其大小(以便计算),然后将其设成预设体(以便之后统一操作),利用ctrl+D复制弄出地形,在Hierarchy视图上创建一个map文件夹保存其文件。然后在地形内删除cube,创出自己想要的敌人行走路径。

    2.在每个转折处设置一个Gameobject文件夹,然后在Transform上放修改一种颜色(以便区分),

    将其命名为Waypoint并设置成预设体,创建一个Waypoint文件夹保存所有的Waypoint。在预设体上加上脚本Waypoint:

    public class Waypoint : MonoBehaviour {
        public static Transform[] p;//定义数组
        private void Awake()//在脚本执行时执行
        {
            //获取每个数组的参数,也就是节点位置
            p = new Transform[transform.childCount];
            for (int i = 0; i < p.Length; i++) {
                p[i] = transform.GetChild(i);
            }
        }
    }

    在Scence场景上新建一个Sphere(当成敌人)然后将其放在起点,加上脚本Enemy:

    public class Enemy : MonoBehaviour {

        public float speed = 10;//设置敌人的速度
        private Transform[] p;//定义数组

    void Start () {
            p = Waypoint.p;//调用Waypoint脚本获取节点的位置信息
        }

    void Update () {
            Move();//每一帧执行方法
    }

    void Move() {
            transform.Translate((p[index].position-transform.position).normalized*Time.deltaTime*speed);//移动,节点到当前位置的向量差的单位差*完成上一帧的时间*速度

            if (Vector3.Distance(p[index].position, transform.position) < 0.2f)//三维坐标,距离(节点,当前位置)小于0.2f的时候执行
            {
                index++;//增加索引,也就获取到下个节点坐标
                if (index > p.Length - 1)//如果大于最后一个节点时执行
                {
                    Destroy(this.gameObject);//销毁物体
                }
            }
        }

    }

    这样就能让物体按照指定的坐标移动起来。

      3. 控制游戏的视野(设置摄像机),首先把摄像机调整到一个合适的状态(向上移动一定的位置,x轴旋转45°),之后来用脚本控制摄像机的移动,添加脚本Move():

    public class move : MonoBehaviour {
    // Use this for initialization
    void Start () {

    }

    // Update is called once per frame
    void Update () {
            float a = Input.GetAxis("Horizontal");//当按下的左右方向(a,d 方向键的左右 4,6)时获取到一个-1到1的值
            float b = Input.GetAxis("Vertical");//当按下的上下方向(w,s 方向键的上下 8,2)时获取到一个-1到1的值
            float m = Input.GetAxis("Mouse ScrollWheel")*-8;//鼠标滚轮缩放
            transform.Translate(new Vector3(a,m,b)*Time.deltaTime*50,Space.World);//转为世界坐标移动
    }
    }

    4.创建一个Gameobject命名为fuhuaqi代表敌人孵化器(用来控制敌人的生成),首先写一个封装类来保存每一波敌人的类型,总数,速度,封装类命名为Bo:

    [System.Serializable]//可序列化,就是能被其他代码引用的意思
    public class Bo {
        public GameObject e;//敌人的类型
        public int count;//敌人的总数
        public float rate;//每个敌人的间隔
    }

    之后再fuhuaqi上写上控制敌人生成的脚本Enemyfuhuaqi:

    public class Enemyfuhuaqi : MonoBehaviour {

        public Bo[] b;//封装类
        public Transform start;//定义开始位置
        public float jiange = 3;//每波的间隔

        private Coroutine coroutine;//定义协同,方便控制协同的启动和停止

        void Start () {
           coroutine= StartCoroutine(Move());//启动协程
        }

        public void Stop()
        {
            StopCoroutine(coroutine);//停止协程
        }

    //协程
        IEnumerator Move() {
            foreach (Bo b in b)
            {
                for (int i = 0; i < b.count; i++)//循环生成
                {
                    GameObject.Instantiate(b.e,start.position,Quaternion.identity);//创建(生成种类,生成位置,不旋转)
                    if (i != b.count - 1)
                    {
                        yield return new WaitForSeconds(b.rate);//协程中的等待(同样敌人的间隔)
                    }
                }    
               yield return new WaitForSeconds(jiange);//完成一波后等待间隔的时间
        }

    }

    写好脚本后记得给相对应的东西赋值!

    5.右键点击Assets->import package->custom package选择载好的材料包,将建筑模型拖入Scence然后在Transform的右侧的齿轮Reset,调整位子(注:选中模型里面所有子物体进行调整,因为之后要按照模型定位与地图方块的地位相同,不让其陷入地板),给对应的位置上材质。做好后设置成Prefab。

    6.开始设置UI界面,首先创建一个Canvas(画布),点击Scence上的2D按钮界面会变成2D界面比较容易操作,在画布里在创建一个Gameobject(名称:Switch)在其下创建3个Canvas存放炮塔的图标在其下再创建image(名:background)和text(用来存放炮塔的图标图片和价格)在其background下创建image(名:Checkmark 用来表示被选中状态)将其中Soure image 属性改成Knob然后改变颜色改变透明度

    给Switch添加属性Toggle group将每个炮塔添加属性Toggle并is on属性都取消,将Checkmark拖入Graphic,将Group都设置成Switch(表示在一个组里面)

    7.在主Canvas下创建Text(名:money)用来显示当前的钱,设置一段动画让钱不够时会闪烁,选择菜单栏上的windows->animation->create->选择保存到Animations文件夹(文件夹自己定义),点击红点开始录制,然后按照喜好变化就好。双击动画进入编辑界面:


    再fuhuaqi上添加脚本buildguanliqi,将定义好的文本和动画赋值上去

    8.创建炮塔,首先选中右侧建造的炮塔,检查mapcube上是否为空,并且鼠标点击mapcube,即可建造。加上粒子效果,建造后money减少。

    9.创建子弹,在头部定义子弹的位置实例化子弹,然后给炮塔加一个Shpere collider用来检测进入的敌人并用数组存放,并且默认攻击第一个,超出范围或者敌人死亡移除该物体在数组中的位置

    10.升级炮塔,检测mapcube上是否有物体存在,物体是否升级过,钱是否够,满足条件这摧毁先前物体,新建升级物体并播放特效。

    11.敌人全部死亡,显示胜利,到达终点则显示失败;

    展开全文
  • unity3D汽车游戏

    热门讨论 2020-07-19 23:34:16
    unity3D做的一个简单的汽车游戏,这是初级版~~~~大家可以下来看一下哦
  • 这套教程涵盖了Unity Mesh编程、模拟水算法(water simulations)、方块移动算法(marching-cubes)等等。这是一套比较有深度的教程,可能需要你了解一些Unity和C#相关的知识。 二、原文链接 原文出处:公众号墙外的...

    一、前言

    这套教程涵盖了Unity Mesh编程、模拟水算法(water simulations)、方块移动算法(marching-cubes)等等。这是一套比较有深度的教程,可能需要你了解一些Unity和C#相关的知识。

    二、效果图

    在这里插入图片描述

    三、正文

    一、基础篇:生成数据块

    预备开始

    首先,我们先来创建一个空的项目,命名随意即可。

    然后创建在Assets下创建一个文件夹,命名为“Scripts”,并创建三个C#脚本,如下图所示:
    在这里插入图片描述
    Chunk用于存储方块数据和创建网格,并且对网格进行渲染和碰撞;Block用于存放方块需要的信息;MeshData用于存储网格数据。

    Chunk.cs脚本:
    在这里插入图片描述
    首先,我们要求该脚本必须包含上个组件:MeshFilter、MeshRenderer、MeshCollider。我们的数据块(就是由一堆方块组成的大方块~)需要这三个组件完成网格的创建和碰撞。

    然后,我们有三个变量。我们有一个Block类型的三维数组变量 blocks,Block类用于存放方块需要的信息,因此我们这个blocks变量就是用于存放一个数据块的方块的信息。

    chunkSize是一个静态变量,它用于表示我们的数据块各个方向的大小(就是长宽高的大小)。

    最后我们有一个bool类型的变量,用于标志该数据块是否在每帧结束后更新。

    其次有三个函数,分别为GetBlock、UpdateChunk、RenderMesh。GetBlock用于获取对应位置的方块;UpdateChunk用于更新数据块的网格数据,然后将更新的数据提供给RenderMesh去渲染。

    MeshData.cs脚本:
    在这里插入图片描述
    由于这个脚本只是为了存储数据,因此不必继承自MonoBehaviour。

    前三个变量(vertices、triangles、uv)是用于渲染网格用的,后两个用于网格碰撞(colVetices、colTriangles)。

    Block.cs脚本:
    在这里插入图片描述
    同样的,Block脚本也不需要继承自MonoBehaviour,并且它将会是所有方块的基类。主要用于存储方块所需信息。

    BlockData函数用于生成该方块的网格信息。

    我们来设想一下,假如我们的数据块是有25个方块组成的,那么在相邻的方块之间,有一些面就不必渲染出来,浪费系统资源。因此,接下来让我们进行剔除多余面数的处理。

    首先,我们需要一个函数去判断两个方块是否相邻,如果相邻则对各个方向的面进行剔除处理。

    在那之前,让我们先定一个表示方向的枚举(Block脚本中):
    在这里插入图片描述
    然后让我们为Block脚本添加判断的函数:
    在这里插入图片描述
    因为Block脚本是所有方块类的基类,所有我们在Block脚本中对IsSolid函数没有进行判断处理,全部返回true。

    现在让我们开始写一些我们的BlockData函数,在这个函数中,我们会根据当前方块对应方向上相邻方块的面进行剔除处理。
    在这里插入图片描述

    上图中的注释也说得很清楚了。举个栗子:判断当前方块顶上相邻的方块是否有底面,如果有则当前方块就不制作顶面,如下图分析所示:
    在这里插入图片描述
    接下来就是添加需要绘制对应的面的函数了。

    上:
    在这里插入图片描述
    上面的注释也说得很清楚了,但是克森还是给你们秀一秀我的美术功底。
    在这里插入图片描述
    其它的面就不细讲,代码如下:

    下:
    在这里插入图片描述
    东:
    在这里插入图片描述
    北:
    在这里插入图片描述
    南:
    在这里插入图片描述
    西:
    在这里插入图片描述
    添加点之后,我们还要把这些点组合成三角形,因此在函数的最后调用了MeshData里的AddQuadTriangles(),由名字可知道,该函数用于添加面片,因此我们要用这四个顶点组合成一个面片,让我们回到MeshData中添加该函数:
    在这里插入图片描述
    再给大家上一次克森的美术作品,相信大家都能理解了吧。
    在这里插入图片描述
    接下来,让我们创建一个新的脚本,命名为“BlockAir”,让它继承值Block类,如下所示:
    在这里插入图片描述
    Okey,现在让我们回到Chunk脚本中,开始添加方块进行测试咯。添加如下代码:
    在这里插入图片描述
    首先声明两个变量,一个为MeshFilter(网格过滤器)类型,另一个为MeshCollider(网格碰撞器)类型。分别用于存储和设置我们对应数据库上的组件属性。

    首先通过GetComponent方法获取对应物体上的对应的组件,然后初始化了我们的数据块(blocks),我们的数据块是一个161616大小的正方体,里面由一堆小方块组成。当前数据库的小方块类型为 BlockAir。

    然后修改了该数据库blocks[3, 5, 2]的方块数据,修改为Block类型方块。

    最后调用UpdateChunk函数进行数据的更新。

    好的,接下来让我们完善我们的UpdateChunk函数:

    首先声明一个类型为MeshData的变量。然后循环遍历blocks进行数据的更新(就是调用每一个方块的BlockData函数,而BlockData函数则是用来处理方块的网格数据,例如剔除面等等)。

    最后就是调用RenderMesh函数将更新好的网格数据传入,然后进行网格的渲染。

    那么,接下来让我们完成我们的RenderMesh函数:
    在这里插入图片描述
    这个函数很简单,就是先调用Clear函数清除上一次网格的数据,然后重新设置即可。

    这一篇只是简单的介绍怎么生成数据块,还没涉及到贴图和碰撞,所以在RenderMesh函数里只是更新了网格数据。下一篇则教大家如何添加贴图到数据块上。

    说那么多,先看看效果。

    首先创建一个空物体,然后为该物体添加Chunk脚本。你将会发现如下效果:
    在这里插入图片描述
    在这里插入图片描述
    为什么方块会跑那去了,为什么会是紫色的呢?因为该方块没有材质,所以是紫色的。因为我们设置了该方块的位置为(3,5,2),那我们是在哪里设置的呢,其实是在这个地方设置了,如下图所示:
    在这里插入图片描述
    这个时候你可以创建一个Cube物体去比对一下就知道了,下图演示:
    在这里插入图片描述
    看来是这样的没错。好,接下来克森带大家来走一走这个运行时候的步骤:

    1.首先我们先进行实例化数据块,也就是Chunk类里面的blocks变量。

    在这里插入图片描述
    2.调用UpdateChunk更新网格数据,在UpdateChunk中又调用了各个方块的BlockData函数生成网格数据。

    在这里插入图片描述
    3.在BlockData中我们对当前方块根据检测相邻方块进行剔除面操作。
    在这里插入图片描述
    (由于函数太大,所以只截一小部分)

    4.最后调用RenderMesh对网格数据进行更新。

    在这里插入图片描述
    在这里,为什么我们只生成了一个方块呢。因为要想生成方块,就必须调用BlockData函数,而BlockAir的BlockData函数里我们只做了一个返回,并没有生成网格数据,

    在这里插入图片描述
    因此只生成了一个方块,也就是我们在后面修改的那个位置为(3,5,2)的方块,因为在Block类里BlockData函数已经生成了网格数据。

    在这里插入图片描述
    为什么克森不直接将所有的方块实例化为Block类呢,原因是这样做会造成数组下标越界。大家还记得下面这个函数吗?
    在这里插入图片描述
    假设当前方块的y为16,这y+1便会越界。对于这个处理,后续的文章中会有介绍。敬请关注吧。

    忘记说最后一点了,对于为什么会生成一个方块呢。原因就是在下图判断中,如果返回为false则制作该方块对应的面,然后我们的BlockAir的IsSolid函数返回的就是false,因此我们的方块就出来了。
    在这里插入图片描述

    二、基础篇:生成贴图

    上一次我们制作了一些函数去设置网格数据,在这篇文章中,我将教大家怎么为方块贴图。

    贴图嘛,首先你得有图才行呀。因此请你将下面这张图右键保存到你的工程目录中:

    在这里插入图片描述

    打开该图片的导入设置,设置图片类型(Texture Type)为 Advanced,然后将所有的勾都去掉,将过滤模式(Filter Mode)设置为 Point(no filter)。最后的设置如下所示:
    在这里插入图片描述
    好,让我们来看看为什么要这么设置呢?大家可从下面几张图中看出效果:
    在这里插入图片描述
    (point模式)
    在这里插入图片描述
    (Bilinear模式)

    为什么会这样呢?总之请大伙们记住的就是,如果做像素游戏那么就选择Point模式就对了。而Bilinear模式会对纹理进行插值计算,会先找出最接近像素的四个图素进行插值运算,这会使得纹理更为平滑;而Point模式只是对纹理进行简单的插值,会使用包含像素最多的部分的图素来贴图,容易出现所谓的“马赛克”想象。而“马赛克”想象正是我们想要的效果。

    接下来拖拽该图片到上一篇创建好的Chunk物体上,这时你会看到工程目录中多出了一个Metarials文件夹,这是由Unity根据拖拽的纹理自动生成的,这边省去了手动创建纹理的时间。

    接下来打开我们的Block脚本,我们需要使用一个结构体去保存纹理信息,以便对纹理坐标进行修改:

    在这里插入图片描述
    接下来我们创建一个函数,该函数用于根据指定方面的面对纹理进行修改,简单的说就是给指定方向上的面贴指定的纹理:

    在这里插入图片描述
    该函数很简单,就是返回一个修改好的Tile类型的结构体,也就是纹理上的坐标位置。

    接下来我们需要一个float类型的产量,用于表示纹理位置的比例:

    在这里插入图片描述
    为什么会是0.25f呢,下图有解释:
    在这里插入图片描述
    如果还不理解,没关系,我们继续往下走。

    下面的函数用于生成对应方向面的UV位置:

    在这里插入图片描述
    好,暂时不解释,接下来再在faceData*(*代表各个方向上的函数)函数中添加如下代码:
    在这里插入图片描述
    最后在Chunk.cs脚本的RenderMesh函数中添加两行代码来渲染我们的UV贴图即可:
    在这里插入图片描述
    先不做解释,先测试一番看看效果:
    在这里插入图片描述
    一个石块便完美的展示出来了。

    好,接下来让我们来分析分析该石块制作的过程:

    首先,用于生成UV位置的主要函数是如下函数:

    在这里插入图片描述
    然后该函数调用了TexturePosition函数来生成UV的 x 和 y 的位置:
    在这里插入图片描述
    在该函数中将 x 和 y 都设置为0。然后让我们回到FaceUVs函数中来,计算计算最终生成UV的位置:
    在这里插入图片描述
    对应我们纹理中的位置如下图所示:
    在这里插入图片描述
    由于我们在各个方向的面都使用了同一个纹理坐标,因此该方块的每个都面都是上图中的纹理,接下来让我们生成文章前头看到的那个草块。

    让我们新建一个C#脚本,命名为“BlockGrass.cs”,双击打开脚本,为其添加如下代码:
    在这里插入图片描述
    其实这个脚本很简单,我们只是在TexturePosition函数中对某个方面上的面做了些特殊操作,仅此而已。

    然后在初始化创建方块的地方为其添加如下代码:
    在这里插入图片描述
    这时候Play游戏,你将会看到如下图所示效果:

    在这里插入图片描述
    至于为什么会生成,前面已经有做过解释,这里就不再赘述。

    三、基础篇:生成网格碰撞

    上一次我们为方块添加了贴图,这一章我将为方块添加网格碰撞。

    从下图可知,我们的方块已经有网格数据了,然而我们的Mesh Collider的Mesh属性还没有任何网格数据。

    在这里插入图片描述

    接下来,让我们来为方块添加网格碰撞。

    提示
    更新网格碰撞数据是很耗性能的,因此我们应该把网格碰撞设计得越简洁越好。如果你不打算使用Unity自带的物理系统,那么你可以百度一下“AABB”碰撞检测算法,这是最简单的碰撞算法之一了。

    因为网格碰撞会随着网格数据的变化而变化,为了方面网格碰撞的数据能与网格数据同步变化,首先在“MeshDta.cs”脚本中添加下面变量:

    在这里插入图片描述
    当该布尔变量为true的时候,我们在对网格添加面片数据的时候,也会对网格碰撞的面片数据进行更新,如下图所示:
    在这里插入图片描述
    请大家注意一下,我们为网格碰撞添加面片的时候使用的是colVertices.Count,而不是vertices.Count。

    那么接下来,我们为网格碰撞添加顶点,如下图所示:

    在这里插入图片描述
    好了,让我们回到“Block.cs”脚本中,修改一下网格顶点的添加方式:如下图所示:
    在这里插入图片描述
    提示
    一定要将“Block.cs”原来添加顶点的方式换成转换的方式!!!

    这样便可在useRenderDataForCol为true的时候,添加网格的顶点数据的同时也添加了网格碰撞的顶点数据,是不是既方便又简单呀。

    网格碰撞的顶点数据添加好了,接下来添加网格碰撞的三角形数据的步骤和添加顶点的步骤相似。

    首先处理一下添加三角形的函数,如下图所示:

    在这里插入图片描述
    提示
    目前我们只使用了AddQuadTriangies()函数,上图的函数也许会在后面的开发中用的,之所以提前写上是因为该函数和本篇文章相关联。

    接下来回到我们的“Block.cs”脚本的添加下面一行代码就可以跑一跑测试了:
    在这里插入图片描述
    将这行代码添上之后,按下ctrl+s,之后回到Unity点击Play按钮,将会看到如下图所示效果:
    在这里插入图片描述
    OK,这一篇结束

    四、基础篇:添加地形管理

    上一次我们为方块添加了网格碰撞,这一章将会创建一个地形管理相关的类。

    首先创建一个名为“WorldPos.cs”的脚本,双击打开,码入如下代码:

    using System.Collections;
    
    public struct WorldPos
    {
        public int x, y, z;
    
    public WorldPos(int x, int y, int z)
        {
            this.x = x;
            this.y = y;
            this.z = z;
        }
        // 重写了 Equals 方法,便于比较和方便字典操作(后面有讲)
        public override bool Equals(object obj)
        {
            if (!(obj is WorldPos))
                return false;
    
    WorldPos pos = (WorldPos)obj;
            if (pos.x != x || pos.y != y || pos.z != z)
            {
                return false;
            }
            else
            {
                return true;
            }
        }
    }
    

    这个脚本里的代码很简单,也就是创建了一个带有三个 int 类型的结构体,然后重写了 Equals 方法,至于为什么这么做,接着往下看便知道了。

    接下来创建一个名为“World.cs”的脚本,双击打开脚本,码入如下代码:

    sing UnityEngine;
    using System.Collections.Generic;
    
    public class World : MonoBehaviour {
    
    // 用来管理 chunk
        public Dictionary<WorldPos, Chunk> chunks = new Dictionary<WorldPos, Chunk>();
        // chunk 预设体,用做创建对象的模板
        public GameObject chunkPrefab;
    }
    

    咳咳咳,到这里就知道为什么了要创建“WorldPos.cs”脚本,和重写 Equals 方法了吧。

    那就是因为我们使用了字典结构来管理我们的 Chunk,而 key 为 WorldPos,我们都知道字典的 key 是唯一的,如果想要得到某个 key 对应的值,那么我们就需要传入相等的 key 才能够得到对应的值,因此我们就需要重写 WorldPos 的 Equals 方法。如果不重写 Equals 方法的话,默认对比两个 WorldPos 则是通过它们各自的 Hash 值来对比的,而每个 new 出来的对象的 Hash 都不相同,所以这就是为什么要重写 Equals 方法的主要原因。

    逼逼那么多,也不知道说得对不对,233。

    Ok,让我们回到 Unity 编辑器中,然后按步骤执行如下操作:
    在这里插入图片描述
    在这里插入图片描述
    Ok,接下来让我们运行 Unity,你会发现什么都没有。

    让我们双击打开“Chunk.cs”脚本,修改如下代码:
    在这里插入图片描述
    嚯嚯嚯,初始化代码都去掉了,我们要怎么创建 chunk 方块呀?

    稍等骂爹,我们不是有一个用于管理 chunk 的类吗?对的,接下来让我们在 “World.cs”脚本里对“Chunk”进行初始化。回到“World.cs”脚本,添加如下代码:

    void Start()
        {
            // 初始化世界
            for (int x = 0; x < 1; x++)
            {
                for (int y = 0; y < 1; y++)
                {
                    for (int z = 0; z < 1; z++)
                    {
                        // 之所以乘上 Chunk.chunkSize,是用于确保每一个 Chunk 的范围
                        CreateChunk(x * Chunk.chunkSize, y * Chunk.chunkSize, z * Chunk.chunkSize);
                    }
                }
            }
        }
    

    上图中的代码较为简单,就不细说了,接下来添加“CreateChunk”函数,如下图所示:

     public void CreateChunk(int x, int y, int z)
        {
            // 创建 WorldPos 对象,并初始化
            WorldPos worldPos = new WorldPos(x, y, z);
    
    // 使用预设体创建游戏对象
            GameObject newChunkObject = Instantiate(
                            chunkPrefab, new Vector3(x, y, z),
                            Quaternion.Euler(Vector3.zero)
                        ) as GameObject;
    
    // 获取对象上的 Chuck 组件
            Chunk newChunk = newChunkObject.GetComponent<Chunk>();
    
    // 为组件赋值
            newChunk.pos = worldPos;
            newChunk.world = this;
    
    // 将该 chuck 添加到字典中管理
            chunks.Add(worldPos, newChunk);
    
    // 这段代码其实就是原来 Chunk.cs 脚本里初始化的代码
            for (int xi = 0; xi < Chunk.chunkSize; xi++)
            {
                for (int yi = 0; yi < Chunk.chunkSize; yi++)
                {
                    for (int zi = 0; zi < Chunk.chunkSize; zi++)
                    {
                        SetBlock(x + xi, y + yi, z + zi, new BlockGrass());
                    }
                }
            }
        }
    

    上图代码基本都上了注释,因此不再细说。接下来添加“SetBlock”函数,代码如下:

     public void SetBlock(int x, int y, int z, Block block)
        {
            // 在这里封装了一层,用于做相关检测逻辑
            Chunk chunk = GetChunk(x, y, z);
    
    if (chunk != null)
            {
                // 调用 chunk 的 SetBlock 函数,其实就是为 chunk 里的 blocks 数组设置对应的值,
                // 只不过也在该函数中做了相关检测处理逻辑
                chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, block);
                chunk.update = true;
            }
        }
    

    接下来先添加“GetChunk”函数,代码如下:

      public Chunk GetChunk(int x, int y, int z)
        {
            // 下面五行代码主要用于计算当前 Block 位置对应的 chunk 于字典中的位置,并为 pos 赋值
            // 因为我们创建 chunk 是使用 CreateChunk(x * Chunk.chunkSize, y * Chunk.chunkSize, z * Chunk.chunkSize); 来创建的
            // 下面的操作只是将其操作反向计算了一下,仅此而已。
            WorldPos pos = new WorldPos();
            float multiple = Chunk.chunkSize;
            pos.x = Mathf.FloorToInt(x / multiple) * Chunk.chunkSize;
            pos.y = Mathf.FloorToInt(y / multiple) * Chunk.chunkSize;
            pos.z = Mathf.FloorToInt(z / multiple) * Chunk.chunkSize;
    
    Chunk chunk = null;
    
    chunks.TryGetValue(pos, out chunk);
    
    return chunk;
        }
    

    接下来让我们回到“Chunk.cs”脚本中,添加如下关联函数,代码如下所示:

     public static bool InRange(int index)
        {
            // 因为我们的 chunk 为一个正方体,
            // 因此这里的逻辑就是判断 x、y、z 的位置时候在该立方体内
            if (index < 0 || index >= chunkSize)
                return false;
    
    return true;
        }
    
    public void SetBlock(int x, int y, int z, Block block)
        {
            // InRange 故名思意就是判断传入的位置是否正确
            if (InRange(x) && InRange(y) && InRange(z))
            {
                blocks[x, y, z] = block;
            }
        }
    

    Ok,码到这里基本功能就完工了,让我们运行 Unity,查看效果,此时会报如下错误:
    在这里插入图片描述
    双击点击错误,便来到了报错的位置。报错的位置于“Chunk.cs”脚本中的“SetBlock”函数,这里为什么会报错,怎么看也没毛病啊。经过克森一系列的猜测,果然如此,组件的一些方法调用顺序出了问题。具体解决方案就是把“Chunk.cs”脚本你的“Start”函数该为“Awake”即可,如下所示:

       void Awake()
        {
            filter = gameObject.GetComponent<MeshFilter>();
            coll = gameObject.GetComponent<MeshCollider>();
            blocks = new Block[chunkSize, chunkSize, chunkSize];
        }
    

    好的,继续运行 Unity,居然还是报错了,错误如下:
    在这里插入图片描述
    双击点击错误,便来到了报错的位置。报错的位置于“Chunk.cs”脚本中的“GetBlock”函数,报错信息为数组下标越界。

    嘿,这个错误非常眼熟呀,之前的文章中也报了这个错误。其实就是因为我们在“Block.cs”脚本的“BlockData”函数中进行了如下判断:
    在这里插入图片描述
    然后我们是在“Chunk.cs”脚本的“UpdateChunk”函数中调用了“BlockData”函数,如下所示:
    在这里插入图片描述
    在这个函数中,我们会传入的最大值为“chunkSize - 1” ,然而在“BlockData”函数中的判断中会进行“* + 1”(*表示 x、y、z 任意一个),最终会传入“GetBlock”函数中的最大参数值为“chunkSize”,因此便造成了数组下标越界。

    Ok,逼逼了那么多,我们该怎么处理了,其实很简单,做个简单的判断即可,这个时候我们的“IsRange”函数便派上用场咯,修改的代码如下所示:

      public Block GetBlock(int x, int y, int z)
        {
            // 判断传入的位置时候位于该 chunk 中,
            // 如果不存在则默认返回一个“BlockAir”方块(因为 BlockAir 方块是一个空方块,不会影响其它逻辑)
            if (InRange(x) && InRange(y) && InRange(z))
                return blocks[x, y, z];
            return new BlockAir();
        }
    

    Ok,这个时候再运行 Unity,便会看到如下图所示:
    在这里插入图片描述
    嚯嚯嚯,看来是成功了,倍儿棒。

    Ok,让我们杂耍一下我们的成果,修改如下代码:
    在这里插入图片描述
    在这里插入图片描述
    运行 Unity,便看到如下图所示:
    在这里插入图片描述
    嚯嚯嚯,是我们想要的效果,不错,可以的,兄Dei。

    接下来让我们看一下生成的 Chunk 网格是怎么样的,具体操作步骤如下所示:
    在这里插入图片描述
    嚯嚯嚯,不错,也是我们想要的效果。chunk 里的多余的面被过滤掉了,这样便节省了贼多性能,哦耶!

    Ok,文章至此基本结束了,最后让我们为“World.cs”脚本添加两个有用的函数,代码如下所示:

        public Block GetBlock(int x, int y, int z)
        {
            Chunk chunk = GetChunk(x, y, z);
    
    if (chunk != null)
            {
                Block block = chunk.GetBlock(
                    x - chunk.pos.x,
                    y - chunk.pos.y,
                    z - chunk.pos.z);
    
    return block;
            }
            else
            {
                return new BlockAir();
            }
        }
    
    
        public void DestroyChunk(int x, int y, int z)
        {
            // 逻辑简单,就是找到指定的 chunk,然后先销毁游戏对象,再移除管理即可(移除对应字典的值)
            Chunk chunk = null;
            if (chunks.TryGetValue(new WorldPos(x, y, z), out chunk))
            {
                Object.Destroy(chunk.gameObject);
                chunks.Remove(new WorldPos(x, y, z));
            }
        }
    

    代码逻辑较为简单,因此就不逼逼了,好了,文章至此就结束吧。

    展开全文
  • Unity3D游戏引擎最详尽基础教程

    万次阅读 2018-06-27 15:23:06
    首先是因为专业,Unity3D非常强大,用它创建一个类似MiniGore这样的3D平面射击游戏简直轻而易举,而就连使命召唤这样的大型3d游戏,如果素材得当也不在话下。二是因为易用,他的操作非常容易,而使用的脚本语言又是...

    我一直向所有想做游戏的朋友推荐Unity3D,为什么呢?首先是因为专业,Unity3D非常强大,用它创建一个类似MiniGore这样的3D平面射击游戏简直轻而易举,而就连使命召唤这样的大型3d游戏,如果素材得当也不在话下。二是因为易用,他的操作非常容易,而使用的脚本语言又是JavaScript或者C#,不仅非常简单,也让各个种类的程序员都非常容易上手。再次,就是因为免费,如果你不使用Pro或者Mobile的内容的话,Unity3d就是完全免费的。还有就是对3D模型的兼容性,几乎任何3d模型都可以导入到unity中,可以说是一个很方便的特性。

     

    今天,我就来讲讲Unity3D的基础操作,这可以说是一个新手必读吧,读完之后,你一定会惊叹,原来做一个游戏这么简单。

     

    第一节 加入重力

     

    我们先打开Unity3d,新建一个场景(Scene),新建的时候应该会有对话框要求你加入哪些Asset Package,记得选择Physics Material,因为后面我们一定要用的哦亲。

     

    如果创建的时候没加,你可以再后来的Asset菜单里加:

     

     

    之后,我们建立一个Cude,调整一下x和z的大小,我这里调成了25,让它变成一个地板,你也可以放一个texture上去,看起来更加真实,我这里从简,再创建一个sphere,在component中选择Physics中的RigidBody,前提是选中你的sphere物体,之后会看到这个Sphere的inpector中加入一个component叫做RigidBody,调整好Camera运行一下,你就会发现Sphere受到重力的影响自动掉落到地板上。

     

     

    之后,通过我们之前引入的Physics Materials资源包,你还可以选择这个物体的物理性质,我这里选择Bouncy(跳跳),落下之后,这个物体就会蹦来蹦去的,呵呵

     

     

     

    第二节 检测碰撞

     

    下面我们通过简单的JavaScript语句来检测碰撞,这在制作游戏中是很有用的,比如说检测到子弹和敌人的碰撞之后,可以使用我们后来要讲到destory命令消灭敌人。

     

    这次我们新建一个Terrain项目,在用Grass贴图来覆盖住它,如果你找不到贴图的话,记得在Asset菜单里找找,看看哪些资源你还没有引入进去。

     

    然后再加入一个Cube项目,我这里叫做PlayerCube,为他加上重力,之后选择bouncy性质,然后我们再新建一个Cude,调整y和z的值让他变成一堵墙,放置好,命名为Wall,别忘了给Terrain改名为Terrain,

     

    之后我们在Asset菜单中建立一个JavaScript项目,改名为CollisionDetect,双击打开,如果你使用的是Unity 3.5的话,就会打开MonoDev,这是一个新工具还是很好用的,在其中新建一个函数,别管Start和Update函数,我们待会会讲。

     

    加入以下代码:

     

    function OnCollisionEnter(theCollision : Collision){
    
     
    
    if(theCollision.gameObject.name=="Terrain"){
    
     
    
    Debug.Log("Hit terrain");
    
     
    
    }else if(theCollision.gameObject.name=="Wall"){
    
     
    
    Debug.Log("Hit the wall");
    
     
    
    }
    
     
    
    }
    

     

    如果看不懂这些代码的话,也没关系,JavaScript是最好学的编程语言,Google一下吧,保存这个JS文件,然后拖到我们的PlayerCube上。

     

    这时候我们运行,就会看到PlayerCube到处蹦,碰到Terrain的时候,控制台就会显示Hit Terrain,碰到wall的时候就会显示Hit the wall。

     

    第三节  检测用户输入

     

    下面我们来讲一下如何检测用户的键盘和鼠标的操作。

     

    就用我们上次的那个场景,这次我们给PlayerCube加上我们可以控制的动作。就是通过“wasd”或者上下左右键来控制移动。

     

    请注意,如果要使用这里使用的方法来控制移动,那么所控制的物体不能够是一个物理世界的物体,也就是说你要把RigidBody这个component删掉,如果你想要在物理世界里移动物体的话,你就需要我们在下面会讲到的力(force)。

     

    首先我们新建一个JavaScript文件,这里命名为InputDetect吧,输入下面的代码:

     

    #pragma strict
    
     
    
    var Player : GameObject;
    
     
    
    function Start () {
    
     
    
    }
    
     
    
    function Update () {
    
     
    
    Player = GameObject.Find("PlayerCube");
    
     
    
    if(Input.GetKey(KeyCode.W)){
    
     
    
    Debug.Log("Hit w");
    
     
    
    Player.transform.Translate(Vector3.forward * 2);
    
     
    
    }else if(Input.GetKey(KeyCode.S)){
    
     
    
    Debug.Log("Hit s");
    
     
    
    Player.transform.Translate(Vector3.back * 2);
    
     
    
    }else if(Input.GetKey(KeyCode.A)){
    
     
    
    Debug.Log("Hit a");
    
     
    
    Player.transform.Translate(Vector3.left * 2);
    
     
    
    }else if(Input.GetKey(KeyCode.D)){
    
     
    
    Debug.Log("Hit d");
    
     
    
    Player.transform.Translate(Vector3.right * 2);
    
     
    
    }
    
     
    
     
    
    }
    

     

    这时,我们运行这个游戏,就可以通过“WASD”来控制PlayerCube的移动了。

     

     

    第四节 使用Prefab复制和控制物体

     

    我们可以直接把Prefab看做是个妈,她能生好多小孩,如果妈妈的DNA变了,小孩的跟着变,就是说你可以用Prefab创建物体,然后通过这个Prefab修改所有这类物体的性质,这对于批量生成的敌人或者NPC来说很有用。

     

    首先,先创建一个Prefab,我这里命名为“Mother”,之后新建一个Sphere物体,当然你也可以建一些其他的物体,之后给这个Sphere加上你想要的性质,我加入了RigidBody和Bouncy,之后将Sphere拖到Mother上,你会发现mother变蓝了,之后你就可以随意拖一些mother物体到屏幕上去,你会发现所有的Prefab创建出的物体在清单上都是蓝色的,说明他们都有共同的性质。

     

     

    当然,作用于一个Prefab上的脚本也会作用到它的子物体上去。

     

    第五节 使用Destroy命令消灭物体

     

    我们游戏中怎么能够没有敌人呢?有敌人就得能够消灭他们,这时候,我们就会使用Destroy命令了。

     

    在这里,我们使用上次Mother生出来的小球球作为敌银,一旦我们的PlayerCube碰撞到了小球的话,就是用Destroy让小球消失。

     

    新建一个JavaScript脚本,输入以下代码:

     

    #pragma strict
    
     
    
    var mother : GameObject;
    
     
    
    function Start () {
    
     
    
    }
    
     
    
    function Update () {
    
     
    
    }
    
     
    
    function OnCollisionEnter(theCollision : Collision){
    
     
    
    mother = GameObject.Find("Mother");
    
     
    
    if(theCollision.gameObject.name=="Mother"){
    
     
    
    Debug.Log("Hit mother");
    
     
    
    Destroy(mother);
    
     
    
    }
    
     
    
    }
    

     

    这段代码很好懂吧,就是检验碰撞,然后销毁碰撞到的Mother物体,别忘了把Destroy的JavaScript文件拖到PlayerCube上去。运行一下,就可以看到碰撞到的物体都被消灭了。

     

     

    第六节 使用Instantiate命令创造物体

     

    我们直接在界面创造的物体都只是会在界面被初始化的时候加载,那么,如果为了节约内存或者其他什么事情,我想要在游戏过程中添加物体,那该怎么办呢?你猜对了,就是用instantiate命令。

     

    这个命令就是英文实例化的意思,下面我们给碰撞之后消失的mother物体继续初始化一些,也就是说消灭了一些,又来了一大波···

     

    为什么非要用mother呢?因为只有拥有Prefab的物体才能够使用这个命令哦亲。

     

    function Update () {
    
     
    
    var instance : GameObject = Instantiate (PreFab,transform.position,transform.rotation);
    
     
    
    }
    

     

    注意:这样的话,每次刷新一帧都会出现一个新的mother物体,你的内存也许会受不了哦,更合理的方法是按下空格键,然后出现一个新的物体,你可以自己做吗?试试看

     

    你可能已经注意到了,每一个初始化的脚本都会默认有两个函数,就是:

    Update()函数 —— 每刷新一帧都会检查并执行

    Start()函数 —— 只在场景初始化的时候执行

     

    第七节 使用Timer计时器

     

    这次,我们要让一个计时器倒数,倒数到0的时候创建一个mother物体,来看Javascript代码吧:

     

    #pragma strict
    
     
    
    var myTimer : float = 5.0;
    
     
    
    function Start () {
    
     
    
    }
    
     
    
    function Update () {
    
     
    
    if (myTimer >= 0){
    
    myTimer -= Time.deltaTime;
    
    guiText.text=(""+myTimer);
    
    }
    
     
    
    if (myTimer <= 0){
    
    guiText.text = ("Done");
    
    }
    
     
    
    }
    

     

    把这个脚本拖到GUItext控件上,你会发现我们所期待的效果。

     

     

    第八节 让物体动起来

     

    其实让物体行动的技巧,我们已经会了,不是吗?

     

    如果你觉得不是的话,没有认真的看上面的文章哦,我就再给你讲一次吧。

     

    请特别注意,这种方法不适合物理世界的物体,如果向移动物理世界的物体,请看下一节:

     

    创建一个Cylinder物体,之后将下面的脚本附着在上面。

     

    #pragma strict
    
     
    
    var myObject : GameObject;
    
     
    
    function Start () {
    
     
    
    }
    
     
    
    function Update () {
    
     
    
    myObject.gameObject.Find("Cylinder");
    
    myObject.transform.Translate(Vector3(0,0,1));
    
     
    
    }
    

     

    第九节 使用物理世界的力

     

    这里是我们讲移动物体的重点,因为大部分的物体移动都是在物理世界完成的,而如果想要在物理世界移动东西,除了position和rotation的移动之外,就是使用“力”了,在高中学过经典物理学的人都知道,要使物体用移动,木有力是不行的,那么我们就开始吧,再新建一个什么物体,我新建了一个Capsule。

     

    之后,我会在这个物体的上方加一个力,让它用力的向下飞,触碰到地之后,通过bouncy这个性质再弹回来。新建一个JavaScript文件,就叫做Force吧,加入如下代码:

     

    #pragma strict
    
     
    
    var Power : float = 500.0;
    
     
    
    function Start () {
    
     
    
    rigidbody.AddForce(Vector3(0,Power,0));
    
     
    
    }
    
     
    
    function Update () {
    
     
    
    }
    

     

    运行之后,会发现capsule其实是个很有幽默感的物体。

     

     

    第十节 使用贴图和材质

     

    贴图和材质能够使你的虚拟3D世界看起来更加真实,不幸的是,需要大量在PS和AI中的设计工作,最近我一想到要打开PS就有点烦···呜呜。

     

    闲话少说,我已经做好了一个贴图,怎么使用在材质上呢?之后我们怎么才能通过材质使用到其他物体之上呢?

     

     

    就像你看到的一样,我是直接拖进去的···

     

    之后我们从新建菜单中选择Material这一项,之后选中material,将贴图拖进material的贴图选框里,之后,就可以拖动这个Material赋给任何物体了。

     

     

    第十一节 使用声效和音乐

     

    音效和音乐对于一个游戏来说是非常重要的,一个寂静无声的世界是无聊的,而且不真实,什么?你说声效和音乐是一个东西?那你就错了,声效是,比如说发射子弹的声音,什么东西爆炸的声音等等,而音乐则是背景音乐什么的,不一样的亲。

     

    对于音乐来说,往往使用立体声音乐,而对于生效来说,我们随便使用一个简单的音乐文件就可以了。

     

    想要在Unity3D中播放音乐,你需要两样东西,一个是监听器,一个是音乐源,监听器是Camera的一个组件,所以你不用担心,你唯一需要增加的就是音乐源。

     

    直接将项目中的音乐拖放到Audio Source项就可以播放,但是如果我们想要在某个特定的点上 播放音乐该怎么办呢?答案就是,写一个脚本,新建一个脚本,输入如下代码:

     

    #pragma strict
    
     
    
    var myClip : AudioClip;
    
     
    
    function Start () {
    
     
    
    AudioSource.PlayClipAtPoint(myClip,transform.position);
    
     
    
    }
    
     
    
    function Update () {
    
     
    
    }
    

     

    之后将这个脚本拖放到我们的物体上,最后,将音频文件拖放到AudioSource选项框中。

     

    第十二节 使用关节

     

    关节在3D游戏之中的应用非常广,主要在于物理世界的重力影响,下面我们就通过关节来制作一个钟摆。

     

    这次我们新建一个场景,松了一口气?刚才的场景是不是太乱了?你做的时候可以整理一下嘛,呵呵

     

    新建一个cube作为地面,之后一个cube放在上空作为摆垂,之后用几个capsule分别用component中的fixed joint连接起来,之后最上面的那个capsule与世界相连,使用Hinge Joint,这样的话,他会默认与世界相连,不用担心,看图

     

     

     

    第十三节 使用Axes

     

    在Edit – Project Setting – input 中可以查看各种形式的input,这些input方法在unity中被叫做Axes。

     

    我们创建一个JavaScript,叫做GetAxes,输入以下内容:

     

    #pragma strict
    
     
    
    function Start () {
    
     
    
    }
    
     
    
    function Update () {
    
     
    
    var Honriz : float = Input.GetAxis("Honrizontal");
    
     
    
    Debug.Log(Honriz);
    
     
    
    }
    

     

    在拖放脚本之后,运行这个场景,看看控制台输出了什么,你就明白了。

     

     

    第十四节 使用触发器

     

    触发器一般用于检测一个物体是否处于一个空间内,他是怎么做到的呢?

     

    首先,他创造一个大的物体,之后将他的randerer属性去掉,他就隐形了,这样,我们再把他的isCollide属性去掉,他就不会发生碰撞了,之后,我们只需要用脚本检测物体是否与这个物体碰撞就可以了。

     

    我们新建一个场景,做一个地板,再做一个底座,叠加起来,再在他们的上空做一个方块,设置rigidbody属性,将底座设置成为我们刚才说的那种隐形的状态,之后新建一个JavaScript脚本,输入代码

     

    #pragma strict
    
     
    
    function Start () {
    
     
    
    }
    
     
    
    function Update () {
    
     
    
    }
    
     
    
    function OnTriggerEnter (myTrigger : Collider) {
    
     
    
    if(myTrigger.gameObject.name == "box"){
    
    Debug.Log("Hit");
    
    }
    
     
    
    }
    

     

    按下运行,会发现可以检测出Hit。

     

     

    第十五节 使用GUI

     

    其实我们已经学过了如何使用GUI text,不信,我们就用屏幕上的字代替我们在上面的程序中的Debug.Log现实的内容。

     

    你自己能做吗?

     

    试试看吧,一个下午写了这么多,有点累。

     

    一定要在Unity中加入一个GUI Text,运行一下,就会发现屏幕上的GUI Text会随着你设定的的内容而变化了。

    失败是什么?没有什么,只是更走近成功一步;成功是什么?就是走过了所有通向失败的路,只剩下一条路,那就是成功的路。
    展开全文
  • Unity3D手游开发实践

    万次阅读 2018-04-29 10:58:53
    虽然基于Unity3D,很多东西同样适用于Cocos。本文从以下10大点进行阐述:架构设计、原生插件/平台交互、版本与补丁、用脚本,还是不用?这是一个问题、资源管理、性能优化、异常与Crash、适配与兼容、调试及开发工具...

    本次分享总结,起源于腾讯桌球项目,但是不仅仅限于项目本身。虽然基于Unity3D,很多东西同样适用于Cocos。本文从以下10大点进行阐述:架构设计、原生插件/平台交互、版本与补丁、用脚本,还是不用?这是一个问题、资源管理、性能优化、异常与Crash、适配与兼容、调试及开发工具、项目运营。


    1.架构设计

    好的架构利用大规模项目的多人团队开发和代码管理,也利用查找错误和后期维护。

    • 框架的选择:需要根据团队、项目来进行选择,没有最好的框架,只有最合适的框架。
    • 框架的使用:统一的框架能规范大家的行为,互相之间可以比较平滑切换,可维护性大大提升。除此之外,还能代码解耦。例如StrangeIOC是一个超轻量级和高度可扩展的控制反转(IoC)框架,专门为C#和Unity编写。已知公司内部使用StrangeIOC框架的游戏有:腾讯桌球欢乐麻将植物大战僵尸Online[https://github.com/strangeioc/strangeioc](https://github.com/strangeioc/strangeioc)

    依赖注入(Dependency Injection,简称DI),是一个重要的面向对象编程的法则来削减计算机程序的耦合问题。依赖注入还有一个名字叫做控制反转(Inversion of Control,英文缩写为IoC)。依赖注入是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。即对象在被创建的时候,由一个运行上下文环境或专门组件将其所依赖的服务类对象的引用传递给它。也可以说,依赖被注入到对象中。所以,控制反转是,关于一个对象如何获取他所依赖的对象的引用,这个责任的反转


    StrangeIOC采用MVCS(数据模型 Model,展示视图 View,逻辑控制 Controller,服务Service)结构,通过消息/信号进行交互和通信。整个MVCS框架跟flash的robotlegs基本一致,(忽略语言不一样)详细的参考[http://www.cnblogs.com/skynet/archive/2012/03/21/2410042.html](http://www.cnblogs.com/skynet/archive/2012/03/21/2410042.html)

    • 数据模型 Model:主要负责数据的存储和基本数据处理
    • 展示视图 View:主要负责UI界面展示和动画表现的处理
    • 逻辑控制 Controller:主要负责业务逻辑处理,
    • 服务Service:主要负责独立的网络收发请求等的一些功能。
    • 消息/信号:通过消息/信号去解耦Model、View、Controller、Service这四种模块,他们之间通过消息/信号进行交互。
    • 绑定器Binder:负责绑定消息处理、接口与实例对象、View与Mediator的对应关系。
    • MVCS Context:可以理解为MVC各个模块存在的上下文,负责MVC绑定和实例的创建工作。

    腾讯桌球客户端项目框架

    1.2代码目录的组织:一般客户端用得比较多的MVC框架,怎么划分目录?

    先按业务功能划分,再按照MVC来划分。"蛋糕心语"就是使用的这种方式。
    先按MVC划分,再按照业务功能划分。"D9"、"宝宝斗场"、"魔法花园"、"腾讯桌球"、"欢乐麻将"使用的这种方式。


    根据使用习惯,可以自行选择。个人推荐"先按业务功能划分,再按照 MVC 来划分",使得模块更聚焦(高内聚),第二种方式用多了发现随着项目的运营模块增多,没有第一种那么好维护。
    Unity项目目录的组织:结合Unity规定的一些特殊的用途的文件夹,我们建议Unity项目文件夹组织方式如下。


    其中,Plugins支持Plugins/{Platform}这样的命名规范:

    • Plugins/x86
    • Plugins/x86_64
    • Plugins/Android
    • Plugins/iOS

    如果存在Plugins/{Platform},则加载Plugins/{Platform}目录下的文件,否则加载Plugins目录下的,也就是说,如果存在{Platform}目录,Plugins根目录下的DLL是不会加载的。
    另外,资源组织采用分文件夹存储"成品资源"及"原料资源"的方式处理:防止无关资源参与打包,RawResource即原始资源,Resource即成品资源。当然并不限于RawResource这种形式,其他Unity规定的特殊文件夹都可以这样,例如Raw Standard Assets。

    1.2公司组件

    • msdk(sns、支付midas、推送灯塔、监控Bugly)
    • apollo
    • apollo voice
    • xlua

    目前我们的腾讯桌球、四国军棋都接入了apollo,但是如果服务器不采用apollo框架,不建议客户端接apollo,而是直接接msdk减少二次封装信息的丢失和带来的错误,方便以后升级维护,并且减少导入无用的代码。

    1.3第三方插件选型

    • NGUI
    • DoTween
    • GIF
    • GAF
    • VectrosityScripts
    • PoolManager
    • Mad Level Manger

    2.原生插件/平台交互

    虽然大多时候使用Unity3D进行游戏开发时,只需要使用C#进行逻辑编写。但有时候不可避免的需要使用和编写原生插件,例如一些第三方插件只提供C/C++原生插件、复用已有的C/C++模块等。有一些功能是Unity3D实现不了,必须要调用Android/iOS原生接口,比如获取手机的硬件信息(UnityEngine.SystemInfo没有提供的部分)、调用系统的原生弹窗、手机震动等等

    2.1C/C++插件

    编写和使用原生插件的几个关键点:

    • 创建C/C++原生插件
      • 导出接口必须是C ABI-compatible函数
      • 函数调用约定
    • 在C#中标识C/C++的函数并调用
      • 标识 DLL 中的函数。至少指定函数的名称和包含该函数的 DLL 的名称。
      • 创建用于容纳 DLL 函数的类。可以使用现有类,为每一非托管函数创建单独的类,或者创建包含一组相关的非托管函数的一个类。
      • 在托管代码中创建原型。使用DllImportAttribute标识 DLL 和函数。 用staticextern修饰符标记方法。
        • 调用 DLL 函数。像处理其他任何托管方法一样调用托管类上的方法。
    • 在C#中创建回调函数,C/C++调用C#回调函数
      • 创建托管回调函数。
      • 创建一个委托,并将其作为参数传递给 C/C++函数。平台调用会自动将委托转换为常见的回调格式。
      • 确保在回调函数完成其工作之前,垃圾回收器不会回收委托。

    那么C#与原生插件之间是如何实现互相调用的呢?在弄清楚这个问题之前,我们先看下C#代码(.NET上的程序)的执行的过程:(更详细一点的介绍可以参见我之前写的博客:http://www.cnblogs.com/skynet/archive/2010/05/17/1737028.html

    • 将源码编译为托管模块;
    • 将托管模块组合为程序集;
    • 加载公共语言运行时CLR;
    • 执行程序集代码。
      注:CLR(公共语言运行时,Common Language Runtime)和Java虚拟机一样也是一个运行时环境,它负责资源管理(内存分配和垃圾收集),并保证应用和底层操作系统之间必要的分离。
      为了提高平台的可靠性,以及为了达到面向事务的电子商务应用所要求的稳定性级别,CLR还要负责其他一些任务,比如监视程序的运行。按照.NET的说法,在CLR监视之下运行的程序属于"托管"(managed)代码,而不在CLR之下、直接在裸机上运行的应用或者组件属于"非托管"(unmanaged)的代码

    这几个过程我总结为下图:



    图 .NET上的程序运行

    回调函数是托管代码C#中的定义的函数,对回调函数的调用,实现从非托管C/C++代码中调用托管C#代码。那么C/C++是如何调用C#的呢?大致分为2步,可以用下图表示:


    • 将回调函数指针注册到非托管C/C++代码中(C#中回调函数指委托delegate)
    • 调用注册过的托管C#函数指针

    相比较托管调用非托管,回调函数方式稍微复杂一些。回调函数非常适合重复执行的任务、异步调用等情况下使用
    由上面的介绍可以知道CLR提供了C#程序运行的环境,与非托管代码的C/C++交互调用也由它来完成。CLR提供两种用于与非托管C/C++代码进行交互的机制:

    • 平台调用(Platform Invoke,简称PInvoke或者P/Invoke),它使托管代码能够调用从非托管DLL中导出的函数。
    • COM 互操作,它使托管代码能够通过接口与组件对象模型 (COM) 对象交互。考虑跨平台性,Unity3D不使用这种方式。

    平台调用依赖于元数据在运行时查找导出的函数并封送(Marshal)其参数。 下图显示了这一过程。


    注意:

    1. 除涉及回调函数时以外,平台调用方法调用从托管代码流向非托管代码,而绝不会以相反方向流动。 虽然平台调用的调用只能从托管代码流向非托管代码,但是数据仍然可以作为输入参数或输出参数在两个方向流动
    2. 图中DLL表示动态库,Windows平台指.dll文件、Linux/Android指.so文件、Mac OS X指.dylib/framework文件、iOS中只能使用.a。后文都使用DLL代指,并且DLL使用C/C++编写

    当"平台调用"调用非托管函数时,它将依次执行以下操作:

    • 查找包含该函数的DLL。
    • 将该DLL加载到内存中。
      查找函数在内存中的地址并将其参数推到堆栈上,以封送所需的数据(参数)。

      注意
      只在第一次调用函数时,才会查找和加载 DLL 并查找函数在内存中的地址。iOS中使用的是.a已经静态打包到最终执行文件中。

    • 将控制权转移给非托管函数。

    2.2Android插件

    Java同样提供了这样一个扩展机制JNI(Java Native Interface),能够与C/C++互相通信。

    注:

    • JNI wiki-https://en.wikipedia.org/wiki/Java_Native_Interface,这里不深入介绍JNI,有兴趣的可以自行去研究。如果你还不知道JNI也不用怕,就像Unity3D使用C/C++库一样,用起来还是比较简单的,只需要知道这个东西即可。并且Unity3D对C/C++桥接器这块做了封装,提供AndroidJNI/AndroidJNIHelper/AndroidJavaObject/AndroidJavaClass/AndroidJavaProxy方便使用等,具体使用后面在介绍。JNI提供了若干的API实现了Java和其他语言的通信(主要是C&C++)。从Java1.1开始,JNI标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互,保证本地代码能工作在任何Java 虚拟机环境下。"
    • 作为知识扩展,提一下Android Java虚拟机。Android的Java虚拟机有2个,最开始是Dalvik,后面Google在Android 4.4系统新增一种应用运行模式ART。ART与Dalvik 之间的主要区别是其具有提前 (AOT) 编译模式。 根据 AOT 概念,设备安装应用时,DEX 字节代码转换仅进行一次。 相比于 Dalvik,这样可实现真正的优势 ,因为 Dalvik 的即时 (JIT) 编译方法需要在每次运行应用时都进行代码转换。下文中用Java虚拟机代指Dalvik/ART。

    C#/Java都可以和C/C++通信,那么通过编写一个C/C++模块作为桥接,就使得C#与Java通信成为了可能,如下图所示:


    注:C/C++桥接器本身跟Unity3D没有直接关系,不属于Android和Unity3D,图中放在Unity3D中是为了代指libunity.so中实现的桥接器以表示真实的情况。

    通过JNI既可以用于Java代码调用C/C++代码,也可用于C/C++代码与Java(Dalvik/ART虚拟机)的交互。JNI定义了2个关键概念/结构:JavaVMJNIENVJavaVM提供虚拟机创建、销毁等操作,Java中一个进程可以创建多个虚拟机,但是Android一个进程只能有一个虚拟机JNIENV是线程相关的,对应的是JavaVM中的当前线程的JNI环境,只有附加(attach)到JavaVM的线程才有JNIENV指针,通过JNIEVN指针可以获取JNI功能,否则不能够调用JNI函数。


    C/C++要访问的Java代码,必须要能访问到Java虚拟机,获取虚拟机有2中方法:

    • 在加载动态链接库的时候,JVM会调用JNI_OnLoad(JavaVM jvm, void reserved),第一个参数会传入JavaVM指针。
    • 在C/C++中调用JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args)创建JavaVM指针。

    所以,我们只需要在编写C/C++桥接器so的时候定义**JNI_OnLoad(JavaVM jvm, voidreserved)方法即可,然后把JavaVM指针保存起来作为上下文使用
    获取到JavaVM之后,还不能直接拿到JNI函数去获取Java代码,必须通过线程关联的JNIENV指针去获取。所以,作为一个好的开发习惯在每次获取一个线程的JNI相关功能时,先调用AttachCurrentThread();又或者
    每次通过JavaVM指针获取当前的JNIENV:java_vm->GetEnv((void)&jni_env, version),一定是已经附加到JavaVM的线程。通过JNIENV可以获取到Java的代码,例如你想在本地代码中访问一个对象的字段(field),你可以像下面这样做:

    • 对于类,使用jni_env->FindClass获得类对象的引用
    • 对于字段,使用jni_env->GetFieldId获得字段ID
    • 使用对应的方法(例如jni_env->GetIntField)获取字段的值

    类似地,要调用一个方法,你step1.得获得一个类对象的引用obj,step2.是方法methodID。这些ID通常是指向运行时内部数据结构。查找到它们需要些字符串比较,但一旦你实际去执行它们获得字段或者做方法调用是非常快的。step3.调用jni_env->CallVoidMethodV(obj, methodID, args)
    从上面的示例代码,我们可以看出使用原始的JNI方式去与Android(Java)插件交互是多的繁琐,要自己做太多的事情,并且为了性能需要自己考虑缓存查询到的方法ID,字段ID等等。幸运的是,Unity3D已经为我们封装好了这些,并且考虑了性能优化。Unity3D主要提供了一下2个级别的封装来帮助高效编写代码:


    注:Unity3D中对应的C/C++桥接器包含在libunity.so中。

    • Level 1:AndroidJNI、AndroidJNIHelper,原始的封装相当于我们上面自己编写的C# Wrapper。AndroidJNIHelper 和AndroidJNI自动完成了很多任务(指找到类定义,构造方法等),并且使用缓存使调用java速度更快。AndroidJavaObject和AndroidJavaClass基于AndroidJNIHelper 和AndroidJNI创建,但在处理自动完成部分也有很多自己的逻辑,这些类也有静态的版本,用来访问java类的静态成员。更详细接口参考帮助文档:http://docs.unity3d.com/ScriptReference/AndroidJNI.htmlhttp://docs.unity3d.com/ScriptReference/AndroidJNIHelper.html
    • Level 2:AndroidJavaObject、AndroidJavaClass、AndroidJavaProxy,这个3个类是基于Level1的封装提供了更高层级的封装使用起来更简单,这个在第三部分详细介绍。

    2.3iOS插件

    iOS编写插件比Android要简单很多,因为Objective-C也是 C-compatible的,完全兼容标准C语言。这些就可以非常简单的包一层 extern "c"{},用C语言封装调用iOS功能,暴露给Unity3D调用。并且可以跟原生C/C++库一样编成.a插件。C#与iOS(Objective-C)通信的原理跟C/C++完全一样:


    除此之外,Unity iOS支持插件自动集成方式。所有位于Asset/Plugings/iOS文件夹中后缀名为.m , .mm , .c , .cpp的文件都将自动并入到已生成的Xcode项目中。然而,最终编进执行文件中。后缀为.h的文件不能被包含在Xcode的项目树中,但他们将出现在目标文件系统中,从而使.m/.mm/.c/.cpp文件编译。这样编写iOS插件,除了需要对iOS Objective-C有一定了解之外,与C/C++插件没有差异,反而更简单。

    3.版本与补丁

    任何游戏(端游、手游)都应该提供游戏内更新的途径。一般游戏分为全量更新/整包更新、增量更新、资源更新。

    • 全量
      android游戏内完整安装包下载(ios跳转到AppStore下载)
    • 增量:主要指android省流量更新
      • 可以使用bsdiff生成patch包
      • 应用宝也提供增量更新sdk可供接入
    • 资源
      Unity3D通过使用AssetBundle即可实现动态更新资源的功能。

    手游在实现这块时需要注意的几点:

    • 游戏发布出一定要提供游戏内更新的途径。即使是删掉测试,保不准这期间需要进行资源或者BUG修复更新。很多玩家并不知道如何更新,而且Android手机应用分发平台多样,分发平台本身也不会跟官方同步更新(特别是小的分发平台)。
    • 更新功能要提供强制更新、非强制更新配置化选项,并指定哪些版本可以不强更,哪些版本必须强更。
    • 当游戏提供非强制更新功能之后,现网一定会存在多个版本。如果需要针对不同版本做不同的更新,例如配置文件A针对1.0.0.1修改了一项,针对1.0.0.2修改了另一项,2个版本需要分别更新对应的修改,需要自己实现更新策略IIPS不提供这个功能。当需要复杂的更新策略,推荐自己编写更新服务器和客户端逻辑,不使用iips组件(其实自己实现也很简单)。

    没有运营经验的人会选择二进制,认为二进制安全、更小,这对端游/手游外网只存在一个版本的游戏适合,对一般不强升版本的手游并不适合,反而会对更新和维护带来很大的麻烦。

    • 配置使用XML或者JSON等文本格式,更利于多版本的兼容和更新。最开始腾讯桌球客户端使用的二进制格式(由excel转换而来),但是随着运营配置格式需要增加字段,这样老版本程序就解析不了新的二进制数据,给兼容和更新带来了很大的麻烦。这样就要求上面提到的针对不同步做不同更新,又或者配置一开始就预留好足够的扩展项,其实不管怎么预留扩展也很难跟上需求的变化,而且一开始会把配置表复杂化但是其实只有一张或者几张才会变更结构。
    • iOS版本的送审版本需要连接特定的包含新内容的服务器,现网服务器还不包含新内容。送审通过之后,上架游戏现网服务器会进行更新,iOS版本需要连接现网服务器而非送审服务器,但是这期间又不能修改客户度,这个切换需要通过服务器下发开关进行控制。例如通过指定送审的iOS游戏版本号,客户端判断本地版本号是否为送审版本,如果是连接送审服务器,否则连接现网服务器。

    4.用脚本,还是不用?这是一个问题

    方便更新,减少Crash(特别是使用C++的cocos引擎)

    通过上面一节【版本与补丁】知道要实现代码更新是非常困难的,正式这个原因客户端开发的压力是比较大的,如果出现了比较严重的BUG必须发强制更新版本,使用脚本可以解决这个问题。
    由于Unity3D手游更新成本比较大,而且目前腾讯桌球要求不能强制更新,这导致新版本的活动覆盖率提升比较慢、出现问题之后难以修复。针对这个情况,考虑引入lua进行活动开发,后续发布活动及修复bug只需要发布lua资源,进行资源更新即可,大大降低了发布和修复问题的成本。
    可选方案还有使用Html5进行活动开发,目前游戏中已经预埋了Html5活动入口,并且已经用来发过"玩家调查"、"腾讯棋牌宣传"等。但是与lua对比,不能做到与Unity3D的深度融合,体验不如使用lua,例如不能操作游戏中的ui、不能完成复杂界面的制作、不能复用已有的功能、玩家付费充值跟已有的也会有差异

    游戏脚本之王——Lua

    在公司内部魔方比较喜欢用lua,火隐忍者(手游)unity+ulua,全民水浒cocos2d-x+lua等等都有使用lua进行开发。我们可以使用公司内部的xlua组件,也可以使用ulua[http://ulua.org/]、UniLua[https://github.com/xebecnan/UniLua]等等。

    5.资源管理

    5.1资源管理器

    • 业务不要直接使用引擎或者系统原生接口,而是封装一个资源管理器负责:资源加载、卸载
    • 兼容Resource.Load与AssetBundle资源互相变更需求,开发期间使用Resource.Load方式而不必打AB包效率更高
    • 加载资源时,不管是同步加载还是异步加载,最好是使用异步编码方式(回调函数或者消息通知机制)。如果哪一天资源由本地加载改为从服务器按需加载,而游戏中的逻辑都是同步方式编码的,改起来将非常痛苦。其实异步编码方式很简单,不比同步方式复杂。

    5.2资源类型

    • 图片/纹理(对性能、包体影响最大因素)
    • 音频
      • 背景音乐,腾讯桌球使用.ogg/.mp3
      • 音效,腾讯桌球使用.wav
    • 数据
      • 文本
      • 二进制
    • 动画/特效

    5.3图片-文件格式与纹理格式

    • 常用的图像文件格式有BMP,TGA,JPG,GIF,PNG等;
    • 常用的纹理格式有R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8, A8R8G8B8等。

    文件格式是图像为了存储信息而使用的对信息的特殊编码方式,它存储在磁盘中,或者内存中,但是并不能被GPU所识别,因为以向量计算见长的GPU对于这些复杂的计算无能为力。这些文件格式当被游戏读入后,还是需要经过CPU解压成R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8, A8R8G8B8等像素格式,再传送到GPU端进行使用。
    纹理格式是能被GPU所识别的像素格式,能被快速寻址并采样。举个例子,DDS文件是游戏开发中常用的文件格式,它内部可以包含A4R4G4B4的纹理格式,也可以包含A8R8G8B8的纹理格式,甚至可以包含DXT1的纹理格式。在这里DDS文件有点容器的意味。OpenGL ES 2.0支持以上提到的R5G6B5,A4R4G4B4,A1R5G5B5,R8G8B8,A8R8G8B8等纹理格式,其中 R5G6B5,A4R4G4B4,A1R5G5B5每个像素占用2个字节(BYTE),R8G8B8每个像素占用3个字节,A8R8G8B8每个像素占用 4个字节。
    基于OpenGL ES的压缩纹理有常见的如下几种实现:

    1. ETC1(Ericsson texture compression),ETC1格式是OpenGL ES图形标准的一部分,并且被所有的Android设备所支持。
    2. PVRTC (PowerVR texture compression),支持的GPU为Imagination Technologies的PowerVR SGX系列。
    3. ATITC (ATI texture compression),支持的GPU为Qualcomm的Adreno系列。
    4. S3TC (S3 texture compression),也被称为DXTC,在PC上广泛被使用,但是在移动设备上还是属于新鲜事物。支持的GPU为NVIDIA Tegra系列。

    5.4资源工具

    有了规范就可以做工具检查,从源头到打包

    • 资源导入检查
    • 资源打包检查

    6.性能优化

    掉帧主要针对GPU和CPU做分析;内存占用大主要针对美术资源,音效,配置表,缓存等分析;卡顿也需要对GPU和CPU峰值分析,另外IO或者GC也易导致。


    6.1工欲善其事,必先利其器

    • Unity Profiler
    • XCode instruments
    • Qualcomm Adreno Profiler
    • NVIDIA PerfHUD ES Tegra

    6.2CPU:最佳原则减少计算

    • 复用,UIScrollView Item复用,避免频繁创建销毁对象
    • 缓存,例如Transform
    • 运算裁剪,例如碰撞检测裁剪
      • 粗略碰撞检测(划分空间——二分/四叉树/八叉树/网格等,降低碰撞检测的数量)
      • 精确碰撞检测(检查候选碰撞结果,进而确定对象是否真实发生碰撞)
      • 休眠机制:避免模拟静止的球
    • 逻辑帧与渲染帧分离
    • 分帧处理
    • 异步/多线程处理

    6.3GPU:最佳原则减少渲染

    • 纹理压缩
    • 批处理减少DrawCall(unity-Static Batching和Dynamic Batching,cocos SpriteBatchNode)
    • 减少无效/不必要绘制:屏幕外的裁剪,Flash脏矩阵算法,
    • LOD/特效分档
    • NGUI动静分离(UIPanel.LateUpdate的消耗)
    • 控制角色骨骼数、模型面数/顶点数
    • 降帧,并非所有场景都需要60帧(腾讯桌球游戏场景60帧,其他场景30帧;天天酷跑,在开始游戏前,FPS被限制为30,游戏开始之后FPS才为60。天天飞车的FPS为30,但是当用户一段时间不点击界面后,FPS自动降)

    6.4内存:最佳原则减少内存分配/碎片、及时释放

    • 纹理压缩-Android ETC1、iOS PVRTC 4bpp、windows DXT5
    • 对象池-PoolManager
    • 合并空闲图集
    • UI九宫格
    • 删除不用的脚本(也会占用内存)

    6.5IO:最佳原则减少/异步io

    • 资源异步/多线程加载
    • 预加载
    • 文件压缩
    • 合理规划资源合并打包,并非texturepacker打包成大图集一定好,会增加文件io时间

    6.6网络:其实也是IO的一种

    使用单线程——共用UI线程,通过事件/UI循环驱动;还是多线程——单独的网络线程?

    • 单线程:由游戏循环(事件)驱动,单线程模式比使用多线程模式开发、维护简单很多,但是性能比多线程要差一些,所以在网络IO的时候,需要注意别阻塞到游戏循环。说明,如果网络IO不复杂的情况下,推荐使用该模式。
      • 在UI线程中,别调用可能阻塞的网络函数,优先考虑非阻塞IO
      • 这是网络开发者经常犯的错误之一。比如:做一个简单如 gethostbyname() 的调用,这个操作在小范围中不会存在任何问题,但是在有些情况中现实世界的玩家却会因此阻塞数分钟之久!如果你在 GUI 线程中调用这样一个函数,对于用户来说,在函数阻塞时,GUI 一直都处于 frozen 或者 hanged 状态,这从用户体验的角度是绝对不允许的。
    • 多线程:单独的网络线程,使用独立的网络线程有一个非常明显的好处,主线程可以将脏活、累活交给网络线程做使得UI更流畅,例如消息的编解码、加解密工作,这些都是非常耗时的。但是使用多线程,给开发和维护带来一定成本,并且如果没有一定的经验写出来的网络库不那么稳定,容易出错,甚至导致游戏崩溃。下面是几点注意事项:
      • 千万千万别在网络线程中,回调主线程(UI线程)的回调函数。而是网络线程将数据准备好,让主线程主动去取,亦或者说网络线程将网络数据作为一个事件驱动主线程去取。当年我在用Cocos2d-x + Lua做魔法花园的手机demo时,就采用的多线程模式,最初在网络线程直接调用主线程回调函数,经常会导致莫名其妙的Crash。因为网络线程中没有渲染所必须的opengl上下文,会导致渲染出问题而Crash。

    6.6包大小

    • 使用压缩格式的纹理/音频
    • 尽量不要使用System.Xml而使用较小的Mono.Xml
    • 启用Stripping来减小库的大小
    • Unity strip level(strip by byte code)
    • Unity3D输出APK,取消X86架构
    • iOS Xcode strip开启

    6.7耗电

    下面影响耗电的几个因素和影响度摘自公司内部的一篇文章。


    7.异常与Crash

    7.1防御式编程

    • 非法的输入中保护你的程序
      • 检查每一输入参数
      • 检查来自外部的数据/资源
    • 断言
    • 错误处理
    • 隔栏
      防不胜防,不管如何防御总有失手的时候,这就需要异常捕获和上报。

    7.2异常捕获


    由于很多错误并不是发生在开发工作者调试阶段,而是在用户或测试工作者使用阶段;这就需要相关代码维护工作者对于程序异常捕获收集现场信息。异常与Crash的监控和上报,这里不介绍Bugly的使用,按照apollo或者msdk的文档接入即可,没有太多可以说的。这里主要透过Bugly介绍手游的几类异常的捕获和分析。

    Unity3D C#层异常捕获

    相比比较简单使用:

    code>Application.RegisterLogCallback/Application.RegisterLogCallbackThreaded

    注册回调函数/在一个新的线程中调用委托。特别注意:保证项目中只有一个Application.RegisterLogCallback注册回调,否则后面注册的会覆盖前面注册的回调!回调函数中stackTrace参数包异常调用栈。

    publicvoidHandleLog(stringlogString, stringstackTrace, LogType type){if(logString == null|| logString.StartsWith(cLogPrefix)){return;}ELogLevel level = ELogLevel.Verbose;switch(type){caseLogType.Exception:level = ELogLevel.Error;break;default:return;}if(stackTrace != null){Print(level, ELogTag.UnityLog, logString + "\n"+ stackTrace);}else{Print(level, ELogTag.UnityLog, logString);}}

    Android Java层异常捕获

    try…catch显式的捕获异常一般是不引起游戏Crash的,它又称为编译时异常,即在编译阶段被处理的异常。编译器会强制程序处理所有的Checked异常,因为Java认为这类异常都是可以被处理(修复)的。如果没有try…catch这个异常,则编译出错,错误提示类似于"Unhandled exception type xxxxx"。
    UnChecked异常又称为运行时异常,由于没有相应的try…catch处理该异常对象,所以Java运行环境将会终止,程序将退出,也就是我们所说的Crash。那为什么不会加在try…catch呢?

    • 无法将所有的代码都加上try…catch
    • UnChecked异常通常都是较为严重的异常,或者说已经破坏了运行环境的。比如内存地址,即使我们try…catch住了,也不能明确知道如何处理该异常,才能保证程序接下来的运行是正确的。

    Uncaught异常会导致应用程序崩溃。那么当崩溃了,我们是否可以做些什么呢,就像Application.RegisterLogCallback注册回调打印日志、上报服务器、弹窗提示用户?Java提供了一个接口给我们,可以完成这些,这就是UncaughtExceptionHandler,该接口含有一个纯虚函数:

    public abstract void uncaughtException (Thread thread, Throwableex)

    Uncaught异常发生时会终止线程,此时,系统便会通知UncaughtExceptionHandler,告诉它被终止的线程以及对应的异常,然后便会调用uncaughtException函数。如果该handler没有被显式设置,则会调用对应线程组的默认handler。如果我们要捕获该异常,必须实现我们自己的handler,并通过以下函数进行设置:
    public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)

    特别注意:多次调用setDefaultUncaughtExceptionHandler设置handler,后面注册的会覆盖前面注册的,以最后一次为准。实现自定义的handler,只需要继承UncaughtExceptionHandler该接口,并实现uncaughtException方法即可。
    static class MyCrashHandler implements UncaughtExceptionHandler{  

    
    @Override  
    public  void uncaughtException(Thread thread, finalThrowable throwable) { 
    // Deal this exception  
    }

    }
    在任何线程中,都可以通过setDefaultUncaughtExceptionHandler来设置handler,但在Android应用程序中,全局的Application和Activity、Service都同属于UI主线程,线程名称默认为"main"。所以,在Application中应该为UI主线程添加UncaughtExceptionHandler,这样整个程序中的Activity、Service中出现的UncaughtException事件都可以被处理。
    捕获Exception之后,我们还需要知道崩溃堆栈的信息,这样有助于我们分析崩溃的原因,查找代码的Bug。异常对象的printStackTrace方法用于打印异常的堆栈信息,根据printStackTrace方法的输出结果,我们可以找到异常的源头,并跟踪到异常一路触发的过程。

    public static String getStackTraceInfo(finalThrowable throwable) {  
    
        String trace = "";
        try{  
            Writer writer = newStringWriter();
            PrintWriter pw = newPrintWriter(writer);
            throwable.printStackTrace(pw);
            trace = writer.toString();
            pw.close();
        } catch(Exception e) {
            return"";
        }
        return trace;
    }

    Android Native Crash

    前面我们知道可以编写和使用C/C++原生插件,除非C++使用try...catch捕获异常,否则一般会直接crash,通过捕获信号进行处理。

    iOS 异常捕获

    跟Android、Unity类似,iOS也提供NSSetUncaughtExceptionHandler 来做异常处理。

    #import "CatchCrash.h"
    @implementation CatchCrash
    voiduncaughtExceptionHandler(NSException *exception)
    {
    // 异常的堆栈信息
    NSArray *stackArray = [exception callStackSymbols];
    // 出现异常的原因
    NSString *reason = [exception reason];
    // 异常名称
    NSString *name = [exception name];
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
    NSLog(@"%@", exceptionInfo);
    NSMutableArray *tmpArr = [NSMutableArray arrayWithArray:stackArray];
    [tmpArr insertObject:reason atIndex:0];
    [exceptionInfo writeToFile:[NSString stringWithFormat:@"%@/Documents/error.log",NSHomeDirectory()]  atomically:YES encoding:NSUTF8StringEncoding error:nil];
    }
    @end

    但是内存访问错误、重复释放等错误引起崩溃就无能为力了,因为这种错误它抛出的是信号,所以还必须要专门做信号处理。

    windows crash

    同样windows提供SetUnhandledExceptionFilter函数,设置最高一级的异常处理函数,当程序出现任何未处理的异常,都会触发你设置的函数里,然后在异常处理函数中获取程序异常时的调用堆栈、内存信息、线程信息等。

    8.适配与兼容

    8.1UI适配

    • 锚点(UIAnchor、UIWidgetAnchor属性)
    • NGUI UIRoot统一设置缩放比例
    • UIStretch

    8.2兼容

    • shader兼容:例如if语句有的机型支持不好,Google nexus 6在shader中使用了if就会crash
    • 字体兼容:android复杂的环境,有的手机厂商和rom会对字体进行优化,去掉android默认字体,如果不打包字体会不现实中文字

    9.调试及开发工具

    9.1日志及跟踪

    事实证明,打印日志(printf调试法)是非常有效的方法。一个好用的日志调试,必备以下几个功能:

    • 日志面板/控制台,格式化输出
    • 冗长级别(verbosity level):ERROR、WARN、INFO、DEBUG
    • 频道(channel):按功能等进行模块划分,如网络频道只接收/显示网络模块的消息,频道建议使用枚举进行命名。
    • 日志同时会输出到日志文件
    • 日志上报(iOS屏蔽文档目录,出了问题也拿不到日志)

    9.2调试用绘图工具

    调试绘图用工具指开发及调试期间为了可视化的绘图用工具,如腾讯桌球开发调试时会使用VectrosityScripts可视化球桌的物理模型(实际碰撞线)帮助调试。这类工具可以节省大量时间及快速定位问题。通常调试用绘图工具包含:

    • 支持绘制基本图形,如直线、球体、点、坐标轴、包围盒等
    • 支持自定义配置,如颜色、粒度(线的粗细/球体半径/点的大小)等

    9.3游戏内置菜单/作弊工具

    在开发调试期间提供游戏进行中的一些配置选项及作弊工具,以方便调试和提高效率。例如腾讯桌球游戏中提供:

    • 游戏内物理引擎参数调整菜单;
    • 修改签到奖励领取天数等作弊工具

    注意游戏内的所有开发调试用的工具,都需要通过编译宏开关,保证发布版本不会把工具代码包含进去

    9.4Unity扩展

    Untiy引擎提供了非常强大的编辑器扩展功能,基于Unity Editor可以实现非常多的功能。公司内部、外部都有非常的开源扩展可用
    公司外部,如GitHub上的:
    UnityEditor-MiniExtension
    Unity-Resource-Checker
    UnityEditorHelper
    MissingReferencesUnity
    Unity3D-ExtendedEditor

    公司内部:
    TUTBeautyUnityUnityDependencyBy

    10.项目运营

    自动构建

    • 版本号——主版本号.特性版本号.修正版本号.构建版本号
      • [构建版本号]应用分发平台升级判断基准
    • 自动构建
      • Android
      • iOS — XUPorter

    公司内部接入SODA即可,建议搭建自己的构建机,开发期间每日N Build排队会死人的,另外也可以搭建自己的搭建构建平台

    统计上报

    • Tlog上报
    • 玩家转化关键步骤统计(重要)
    • Ping统计上报
    • 游戏业务的统计上报(例如桌球球局相关的统计上报)
    • 灯塔自定义上报

    运营模板

    • 配置化
    • 服务器动态下发
    • CDN拉取图片并缓存

    上线前的checklist

    项目要点说明指标
    灯塔上报1. 灯塔自带统计信息 2. 自定义信息上报灯塔里面包含很多统计数据,需要检查是否ok1. 版本/渠道分布 2. 使用频率统计 3. 留存统计(1天留存、3天留存、7天留存、14天留存) 4. 用户结构统计(有效用户、沉默用户、流失用户、回流用户、升级用户、新增用户) 5. 硬件统计(机型+版本、分辨率、操作系统、内存、cpu、gpu) 6. Crash统计(Crash版本、Crash硬件、Crash次数等)等等
    信鸽推送能够针对单个玩家,所有玩家推送消息  
    米大师支付正常支付  
    安全组件1. TSS组件接入 2. 隐藏内部符号表:C++开发的代码使用strip编绎选项,抹除程序的符号 3. 关键数据加密,如影子变量+异或加密算法项根据安全中心提供的文档完成所有接入安全组件,并通过安全中心的验收
    稳定性crash率用户crash率:发生CRASH的用户数/使用用户数;启动crash率:启动5S内发生crash用户数/使用用户数低于3%
    弱网络 断线重连考虑,缓存消息,重发机制等等客户端的核心场景必须有断线重连机制,并在有网络抖动、延时、丢包的网络场景下,客户端需达到以下要求:一. 不能出现以下现象:1、游戏中不能出现收支不等、客户端卡死/崩溃等异常情况;2、游戏核心功能(如登录、单局、支付等)不能有导致游戏无法正常进行的UI、交互问题;3、不能有损害玩家利益或可被玩家额外获利的问题;4、需要有合理的重连机制,避免每次重连都返回到登录界面。二. 需要对延时的情况有相应的提示
    兼容性  通过适配测试
    游戏更新1. 整包更新;2. 增量更新 特别说明:iOS送审版本支持连特定环境,与正式环境区别开,需要通过服务器开关控制
    性能内存、CPU、帧率、流量、安装包大小 【内存占用要求】Android平台:在对应档次客户端最低配置以上,均需满足以下内存消耗指标(PSS):
    1. 档机型指标:最高PSS<=300MB (PSS高于这个标准会影响28%用户的体验,约1800万)
    2. 档机型指标:最高PSS<=200MB(PSS高于这个标准会影响45%用户的体验,约3000万)
    3. 档机型指标:最高PSS<=150MB(PSS高于这个标准会影响27%用户的体验,约1800万)
    iOS平台:在对应档次客户端最低配置以上,均需满足以下内存消耗指标(PSS):
    1. 档机型指标:消耗内存(real mem)不大于250MB(高于这个标准会影响53%用户的体验,约1900万)
    2. 档机型指标:消耗内存(real mem)不大于200MB(高于这个标准会影响47%用户的体验,约1700万)
    【CPU占用要求】Android平台:CPU占用(90%)小于60% iOS平台:CPU占用(90%)小于80%
    【帧率要求】
    1. 档机型(CPU为四核1.4GHZ,RAM为2G)或以上机型:游戏核心玩法中,最小FPS应不小于25帧/秒 
    2. 档机型(CPU为两核1.1GH,RAM为768M)或以上机型:游戏核心玩法中,最小FPS应不小于25帧/秒 
    3. 档机型(CPU为1GHZ,RAM为768M)或以上机型:游戏核心玩法中,最小FPS应不小于18帧/秒
    【流量消耗要求】游戏核心玩法流量消耗情况(非一次性消耗)应满足以下条件:
    1. 对于分局的游戏场景,单局消耗流量不超过200KB 
    2. 对于不分局游戏场景或流量与局时有关的场景,10分钟消耗流量不超过500KB
     
    展开全文
  • 游戏的实现本身并不困难,没有涉及到太多技术层面的问题,重点在于关卡的编排和设计,因此我使用了unity自带的默认3D物体来代替各种物品,并写了简单的控制操作脚本,代码如下: public class MoveController : ...
  • 《新仙剑OL》采用跨平台Unity3D引擎,耗资数千万,历时三年多,由台湾大宇正版授权,“仙剑之父”姚壮宪监制的全球首款Unity3D航母级双端(网页和客户端)中国风MMORPG网络游戏巨作。主打温情牌并且延续了仙剑系列的...
  • 游戏会为玩家呈现一个“故事卡”。故事卡上包含一些文字,其中一部分是用于描述玩家当前的状态,另外一部分是在当前情况下玩家可以做出的一系列选择。 根据玩家的不同选择,剧情也会按照不同的分支向前发展,并持续...
  • Unity3D开发小游戏】《战棋小游戏》Unity开发教程

    万次阅读 多人点赞 2020-04-10 16:49:52
    这次想要做的一个小游戏,或者说一个小Demo,其实是一个简单且传统的战棋战斗场景。初步的设计是:在2D世界里创建一张由六边形地块组成的战斗地图,敌我双方依据体力在地图上轮流行动并向对方发动攻击,先消灭掉所有...
  • Unity3D-----简易游戏项目开发01

    千次阅读 2020-06-07 10:25:38
    城市激斗一、简介1、所需知识点2、游戏需求 一、简介 1、所需知识点 (1)射线检测 (2)Mecanim动画系统 (3)Navigation寻路系统 (4)OGUI (5)欧拉角与四元数、向量 (6)持久化数据 2、游戏需求 (1)、开始...
  • 实现角色动画和移动,场景管理和场景切换,按钮事件,以及对象碰撞等内容,适合刚刚学习unity3D的人
  • 每周更新unity3d视频教程,从入门到就业,官方unity3d培训,上千门实时更新课程,供学员在线观看学习,unity3d游戏开发,可以让学员随时随地学习! 免费网上学习unity3d自学教程,国内名师机构专业授课,O基础快速学习,1小时...
  • 这个工具呢,博主在Unity3D游戏开发之反编译AssetBundle提取游戏资源这篇文章中其实已经提到过了,不过因为有些朋友对如何使用这个工具依然存在问题,所以博主决定特地写一篇文章来讲解如何使用disunity来提取Unity...
  • 2018年Unity3D游戏开发前景预测

    万次阅读 2018-01-18 18:16:14
    随着移动手机游戏盛行,游戏开发人才供不应求,如何成为一名优秀的开发者?掌握Unity3D开发技术是...学习Unity3D游戏开发,未来职业发展前景非常可观。Unity 3D是什么?Unity 3D到底是什么?如此受欢迎,市场占有率如此
  • [Unity3D]Unity3D游戏开发之截屏保存精彩瞬间

    万次阅读 热门讨论 2014-09-10 20:37:27
    各位朋友,大家好,我是秦元培,欢迎大家关注我的博客,我的博客地址是blog.csdn.net/...比如博主在写《[Unity3D]Unity3D游戏开发之自由视角下的角色控制》和《[Unity3D]Unity3D游戏开发之角色控制漫谈》这两篇
  • [Unity3D]Unity3D 游戏开发之碰撞检测

    万次阅读 2014-06-13 21:10:37
    大家好,欢迎大家关注由我为大家带来的Unity3D游戏开发系列文章,我的博客地址为:http://blog.csdn.net/qinyuanpei。 今天我们来一起来学习Unity3D中一个很重要的概念:碰撞。为什么说碰撞很重要呢?因为在游戏中...
  • Unity3D游戏开发之SQLite让数据库开发更简单

    万次阅读 多人点赞 2015-11-03 08:09:40
    在经历了一段时间的忙碌后,博主终于有时间来研究新的东西啦,今天博客向和大家一起交流的内容是在Unity3D游戏开发中使用SQLite进行数据库开发,坦白来讲,在我的技术体系中Web和数据库是相对薄弱的两个部分,因此...
  • [Unity3D]Unity3D游戏开发之跑酷游戏项目讲解

    万次阅读 多人点赞 2015-01-04 14:43:03
    大家晚上好,我是秦元培,欢迎大家关注我的博客,我的博客地址是blog.csdn.net/...所以,今天的博客的主题就是《Unity3D游戏开发之跑酷游戏项目讲解》。从博主自身来考虑这件事情,当你选择做自己热爱的事情的时
  • Unity3D游戏资源的提取

    万次阅读 2014-09-16 22:37:07
    前言 ...国内一些比较小的Unity3D游戏基本上资源文件都打包在了apk的assets文件夹中,如下图中的《爸爸去哪儿2》资源结构。 我们知道,国外的游戏一般体验较高 画质一流,所以资源文件会更大
  • 对于编程零基础小白来说,Unity3D游戏开发真的是高深到无法踏足的领域吗?我认为并不是这样的。 按照我个人的理解:其实Unity本身并不是一种体系,而是计算机图形学+游戏开发+各种其他系统的一种表现。学习进阶游戏...
1 2 3 4 5 ... 20
收藏数 47,413
精华内容 18,965
关键字:

unity3d游戏