• 背景 通常游戏的主场景包含的资源较多,这会导致加载场景的时间较长。为了避免这个问题,可以首先加载Loading场景,然后再通过Loading场景来...在Unity中可以通过调用Application.LoadLevelAsync函数来异步加载游戏

    背景

    通常游戏的主场景包含的资源较多,这会导致加载场景的时间较长。为了避免这个问题,可以首先加载Loading场景,然后再通过Loading场景来加载主场景。因为Loading场景包含的资源较少,所以加载速度快。在加载主场景的时候一般会在Loading界面中显示一个进度条来告知玩家当前加载的进度。在Unity中可以通过调用Application.LoadLevelAsync函数来异步加载游戏场景,通过查询AsyncOperation.progress的值来得到场景加载的进度。

    尝试——遇到问题

    第一步当加载完Loading场景后,调用如下的LoadGame函数开始加载游戏场景,使用异步加载的方式加载场景1(Loading场景为0,主场景为1),通过Unity提供的Coroutine机制,我们可以方便的在每一帧结束后调用SetLoadingPercentage函数来更新界面中显示的进度条的数值。

    public void LoadGame() {
        StartCoroutine(StartLoading_1(1));
    }
    
    private IEnumerator StartLoading_1(int scene) {
        AsyncOperation op = Application.LoadLevelAsync(scene);
        while(!op.isDone) {            
            SetLoadingPercentage(op.progress * 100);
            yield return new WaitForEndOfFrame();
        }        
    }
    

    最后进度条的效果显示如下:

    进度条并没有连续的显示加载的进度,而是停顿一下切换一个数字,再停顿一下切换一个数子,最后在没有显示100%就情况下就切换到主场景了。究其原因在于Application.LoadLevelAsync并不是真正的后台加载,它在每一帧加载一些游戏资源,并给出一个progress值,所以在加载的时候还是会造成游戏卡顿,AsyncOperation.progress的值也不够精确。当主场景加载完毕后Unity就自动切换场景,所以上述代码中的while循环体内的代码是不会被调用的,导致进度条不会显示100%。

    修补——100%完成

    为了让进度条能显示100%,取巧一点的办法是将AsyncOperation.progress的值乘上2,这样当加载到50%的时候界面上就显示100%了。缺点是当界面上显示100%的时候,用户还要等待一段时间才会进入游戏。其实Unity提供了手动切换场景的方法,把AsyncOperation.allowSceneActivation设为false就可以禁止Unity加载完毕后自动切换场景,修改后的StartLoading_2代码如下:

    // this function is not work
    private IEnumerator StartLoading_2(int scene) {
        AsyncOperation op = Application.LoadLevelAsync(scene);
        op.allowSceneActivation = false;
        while(!op.isDone) {
            SetLoadingPercentage(op.progress * 100);
            yield return new WaitForEndOfFrame();
        }
        op.allowSceneActivation = true;   
    }
    

    我们首先将AsyncOperation.allowSceneActivation设为false,当加载完成后再设为true。代码看上去没有错,但是执行的结果是进度条最后会一直停留在90%上,场景不会切换。通过打印log发现AsyncOperation.isDone一直为falseAsyncOperation.progress的值增加到0.9后就保持不变了,也就是说场景永远不会被加载完毕。

    在这个帖子中找到了答案,原来把allowSceneActivation设置为false后,Unity就只会加载场景到90%,剩下的10%要等到allowSceneActivation设置为true后才加载,这不得不说是一个坑。所以代码改为如下。当AsyncOperation.progress到达0.9后,就直接把进度条的数值更新为100%,然后设置AsyncOperation.allowSceneActivationture,让Unity继续加载未完成的场景。

    private IEnumerator StartLoading_3(int scene) {
        AsyncOperation op = Application.LoadLevelAsync(scene);
        op.allowSceneActivation = false;
        while(op.progress < 0.9f) {
            SetLoadingPercentage(op.progress * 100);
            yield return new WaitForEndOfFrame();
        }
        SetLoadingPercentage(100);
        yield return new WaitForEndOfFrame();
        op.allowSceneActivation = true;   
    }
    

    最后的效果如下:

    打磨——增加动画

    上述的进度条虽然解决了100%显示的问题,但由于进度条的数值更新不是连续的,所以看上去不够自然和美观。为了看上去像是在连续加载,可以每一次更新进度条的时候插入过渡数值。这里我采用的策略是当获得AsyncOperation.progress的值后,不立即更新进度条的数值,而是每一帧在原有的数值上加1,这样就会产生数字不停滚动的动画效果了,迅雷中显示下载进度就用了这个方法。

    private IEnumerator StartLoading_4(int scene) {
        int displayProgress = 0;
        int toProgress = 0;
        AsyncOperation op = Application.LoadLevelAsync(scene);
        op.allowSceneActivation = false;
        while(op.progress < 0.9f) {
            toProgress = (int)op.progress * 100;
            while(displayProgress < toProgress) {
                ++displayProgress;
                SetLoadingPercentage(displayProgress);
                yield return new WaitForEndOfFrame();
            }
        }
    
        toProgress = 100;
        while(displayProgress < toProgress){
            ++displayProgress;
            SetLoadingPercentage(displayProgress);
            yield return new WaitForEndOfFrame();
        }
        op.allowSceneActivation = true;
    }
    

    displayProgress用来记录要显示在进度条上的数值,最后进度条的动画如下:

    对比第一种的进度条

    总结

    如果在加载游戏主场景之前还需要解析数据表格,生成对象池,进行网络连接等操作,那么可以给这些操作赋予一个权值,利用这些权值就可以计算加载的进度了。如果你的场景加载速度非常快,那么可以使用一个假的进度条,让玩家看上几秒钟的loading动画,然后再加载场景。总之进度条虽然小,但要做好也是不容易的。

    参考

    1. 阿高.Unity 显示Loading(加载)进度 对于网上流行的方法进行更正
    2. Unity3d官方论坛.using allowSceneActivation
    展开全文
  • 异步加载场景分为A、B、C三个场景A场景是开始场景;B场景是加载场景(进度条加载显示);C场景是目标场景在A场景中添加一个按钮,触发函数://异步加载新场景 public void LoadNewScene() { ...

    异步加载场景分为A、B、C三个场景

    A场景是开始场景;B场景是加载场景(进度条加载显示);C场景是目标场景

    在A场景中添加一个按钮,触发函数:

    //异步加载新场景  
    public void LoadNewScene()  
    {  
        //保存需要加载的目标场景  
        Globe.nextSceneName = "Scene";  
      
        SceneManager.LoadScene("Loading");        
    }  



    在B场景中添加一个脚本(AsyncLoadScene),挂载到Camera下

    using System.Collections;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    using UnityEngine.UI;
    
    public class Globe
    {
        public static string nextSceneName;
    
        /// <summary>
        /// 异步加载场景
        /// </summary>
        /// <param name="sceneName">场景名字</param>
        /// <param name="isAsync">是否使用异步加载</param>
        public static void LodingScene(string sceneName, bool isAsync = true)
        {
            //清理Unity垃圾
            Resources.UnloadUnusedAssets();
            //清理GC垃圾
            System.GC.Collect();
            //是否使用异步加载
            if (isAsync)
            {
                //赋值加载场景名称
                nextSceneName = sceneName;
                //跳转到LoadingScene场景
                SceneManager.LoadScene("LoadingScene");
            }
            else
            {
                SceneManager.LoadScene(sceneName);
            }
        }
        
    }
    /// <summary>
    /// 异步加载脚本
    /// </summary>
    public class AsyncLoadScene : MonoBehaviour
    {
        public Slider loadingSlider;
    
        //public Text Tips;
        //public Text loadingText;
    
        //private float loadingSpeed = 1;
    
        //private float targetValue;
    
        private AsyncOperation operation;
    
        private int displayProgress;
        private int toProgress;
    
        void Start()
        {
            loadingSlider.value = 0;
            loadingSlider.maxValue = 100;
    
            if (SceneManager.GetActiveScene().name == "LoadingScene")
            {
                //启动协程  
                StartCoroutine(AsyncLoading());
            }
        }
        private IEnumerator AsyncLoading()
        {
            operation = SceneManager.LoadSceneAsync(Globe.nextSceneName);
            operation.allowSceneActivation = false;
            while (operation.progress < 0.9f)
            {
                toProgress = (int)operation.progress * 100;
                while (displayProgress < toProgress)
                {
                    ++displayProgress;
                    SetLoadingPercentage(displayProgress);
                    yield return new WaitForEndOfFrame();
                }
            }
            toProgress = 100;
            while (displayProgress < toProgress)
            {
                ++displayProgress;
                SetLoadingPercentage(displayProgress);
                yield return new WaitForEndOfFrame();
            }
            operation.allowSceneActivation = true;
        }
    
        private void SetLoadingPercentage(int Percentage)
        {
            loadingSlider.value = Percentage;
            //loadingText.text = Percentage + "%";
        }
    
      
    }



    这里需要注意的是使用AsyncOperation.allowSceneActivation属性来对异步加载的场景进行控制

    为true时,异步加载完毕后将自动切换到C场景

    最后附上效果图




    展开全文
  • 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
     
    展开全文
  • Unity3D脚本--常用代码集

    首先给大家分享一个巨牛巨牛的人工智能教程,是我无意中发现的。教程不仅零基础,通俗易懂,而且非常风趣幽默,还时不时有内涵段子,像看小说一样,哈哈~我正在学习中,觉得太牛了,所以分享给大家!点这里可以跳转到教程

                   

    1. 访问其它物体

    1) 使用Find()和FindWithTag()命令
    Find和FindWithTag是非常耗费时间的命令,要避免在Update()中和每一帧都被调用的函数中使用。在Start()和Awake()中使用,使用公有变量把它保存下来,以供后面使用。如:
      公有变量= GameObject.Find("物体名"); 或
      公有变量= GameObject.FindWithTag("物体标签名");
    2) 把一个物体拖到公有变量上
    3) 引用脚本所在的物体的组件上的参数,如下所示:

       transform.position = Vector3(0,5,4);   renderer.material.color = Color.blue;   light.intensity = 8;

    4) SendMessge()命令:一个调用其它物体上指令(即物体上的脚本中的函数)的方法

    5) GetComponent()命令:引用一个组件

    实例代码如下所示:

    #pragma strictvar thelight:GameObject;var thetxt:GameObject;var theCube1:GameObject;function Start () { thelight = GameObject.Find("Spotlight"); thetxt = GameObject.FindWithTag("txt"); theCube1 = GameObject.Find("Cube1");}function Update () { if(Input.GetKey(KeyCode.L)){  thelight.light.intensity += 0.01;  thetxt.GetComponent(GUIText).text = "当前亮度:"+thelight.light.intensity; }  if(Input.GetKey(KeyCode.K)){  thelight.light.intensity -= 0.01;  thetxt.GetComponent(GUIText).text = "当前亮度:"+thelight.light.intensity; } if(Input.GetKey(KeyCode.S)){  theCube1.SendMessage("OnMouseDown"); //调用theCube1所有脚本中的OnMouseDown函数 }}

    2. 制作第一人称控制器

        第一人称控制器脚本代码如下所示:

    #pragma strictvar speed:float=6.0;var jumpspeed:float=8.0;var gravity:float=20.0;private var movedirection:Vector3=Vector3.zero;private var grounded:boolean=false;function Start () {}function FixedUpdate () { if(grounded){  movedirection=Vector3(Input.GetAxis("Horizontal"),0,Input.GetAxis("Vertical"));    //Transforms direction from local space to world space.  movedirection=transform.TransformDirection(movedirection);    movedirection *= speed;    if(Input.GetButton("Jump")){   movedirection.y = jumpspeed;  } }  movedirection.y -= gravity*Time.deltaTime; //Move command  var controller:CharacterController = GetComponent(CharacterController); var flags = controller.Move(movedirection*Time.deltaTime); //CollisionFlags.CollidedBelow  底部发生了碰撞 //CollisionFlags.CollidedSides 四周发生了碰撞 //CollisionFlags.CollidedAbove 顶端发生了碰撞 grounded = (flags & CollisionFlags.CollidedBelow)!=0;}//强制Unity为本脚本所依附的物体增加角色控制器组件@script RequireComponent(CharacterController)


    3. 导入3DMax模型

        在3DMax(.fbx文件)把显示单位和系统单位设置为cm,再导入到Unity3D(单位为m)中时,则大小相同。

    4. 交互功能(自动开关门)

    4.1 ControllerColliderHit(碰撞检测)    

        玩家撞到门时,门才能开。

        其相关代码如下所示,需要对模型的动画进行分割,以下脚本绑在玩家身上:

    #pragma strictprivate var doorisopen:boolean = false;private var doortimer:float = 0.0;private var currentdoor:GameObject;var door_open_time:float = 5.0;var door_open_sound:AudioClip;var door_shut_sound:AudioClip;function Update(){ if(doorisopen){  doortimer += Time.deltaTime;    if(doortimer > door_open_time) {   doortimer = 0.0;   door(false,door_shut_sound,"doorshut",currentdoor);  } }}//检测玩家是否与门相撞function OnControllerColliderHit(hitt:ControllerColliderHit){ //hitt为与玩家相撞的碰撞体 print("test1"); if(hitt.gameObject.tag == "playerDoor" && doorisopen == false){  print("test2");  door(true,door_open_sound,"dooropen",hitt.gameObject);  currentdoor = hitt.gameObject; }}function door(doorcheck:boolean,a_clip:AudioClip,ani_name:String,thisdoor:GameObject){ doorisopen=doorcheck; thisdoor.audio.PlayOneShot(a_clip); //声音取消3D效果,否则无法播放 thisdoor.transform.parent.animation.Play(ani_name);}

    4.2 RaycastHit(光线投射)

          玩家必须面对门时,光线投射到门时,门才能开。

          其检测碰撞代码如下所示,以下脚本绑在玩家身上

    function Update(){ var hit:RaycastHit; //hit为与投射光线相遇的碰撞体 if(Physics.Raycast(transform.position,transform.forward,hit,3)){    print("collider happen" + hit.collider.gameObject.tag);  if(hit.collider.gameObject.tag == "playerDoor"){   var currentdoor:GameObject = hit.collider.gameObject;   currentdoor.SendMessage("doorcheck");   print("call doorcheck");  } }}


           门身上的脚本如下所示:

    #pragma strictprivate var doorisopen:boolean = false;private var doortimer:float = 0.0;//private var currentdoor:GameObject;var door_open_time:float = 5.0;var door_open_sound:AudioClip;var door_shut_sound:AudioClip;function Update(){ if(doorisopen){  doortimer += Time.deltaTime;    if(doortimer > door_open_time) {   doortimer = 0.0;   door(false,door_shut_sound,"doorshut");  } }}function doorcheck(){ if(!doorisopen){  door(true,door_open_sound,"dooropen"); }}function door(doorcheck:boolean,a_clip:AudioClip,ani_name:String){ doorisopen=doorcheck; audio.PlayOneShot(a_clip); //声音取消3D效果,否则无法播放 transform.parent.animation.Play(ani_name);}


    4.3 OnTriggerEnter(触发器)

          只要一进入触发区域,门就可以打开。需要特别注意调整小房子的Box Collider的大小和位置。

          以下脚本绑在门所在的小房子上:

    #pragma strictfunction OnTriggerEnter (col:Collider) { //col为闯入此区域的碰撞体 if(col.gameObject.tag == "Player"){  transform.FindChild("door").SendMessage("doorcheck"); }}


    4.4 OnCollisionEnter

                 检测两个普通碰撞体之间的碰撞,需要发力,才能发生碰撞。不能与第一人称控制器发生碰撞。


    4.5 交互总结

          1) OnControllerColliderHit

              专门提供给角色控制器使用,以供角色控制器检测与其它物体发生了碰撞。

              它与触发模式的碰撞体不能发生碰撞,而且还会穿过其物体。

               以下脚本绑定在玩家(第一人称控制器)身上。

    function OnControllerColliderHit (hit:ControllerColliderHit) { if(hit.gameObject.name != "Plane") gameObject.Find("wenzi").guiText.text = hit.gameObject.name;}


          2) RaycastHit

              它不是一个事件,而是一个命令,可以需要的地方调用它来发射一条光线来进行检测。

               可以与触发模式的碰撞体发生碰撞,但也会穿过其物体。   

               以下脚本绑定在玩家(第一人称控制器)身上。            

    #pragma strictvar hit:RaycastHit;function Update () { if(Physics.Raycast(transform.position,transform.forward,hit,2)){  if(hit.collider.gameObject.name != "Plane"){   gameObject.Find("wenzi").guiText.text = hit.collider.gameObject.name;  } }}

          3) OnTriggerEnter

              以下脚本绑定在与第一人称控制器相撞的物体上。

    #pragma strictfunction OnTriggerEnter (col:Collider) { gameObject.Find("wenzi").guiText.text = col.gameObject.name;}

            4) OnCollisionEnter

                 检测两个普通碰撞体之间的碰撞,需要发力,才能发生碰撞。不能与第一人称控制器发生碰撞。

    5. 播放声音

    5.1 audio.Play()

        GameObject有Audio Source组件,且在Audio Source组件中设置了Audio Clip。

         

    5.2 audio.PlayOneShot

    var door_open_sound:AudioClip; //在检测面板中设置audio clip;当然必须有Audio Source组件,但不需要设置Audio Clipfunction myAudioPlay(){ audio.PlayOneShot(door_open_sound); //声音取消3D效果,否则无法播放}


    5.3 AudioSource.PlayClipAtPoint

          此种播放方式最佳,节约资源。

    var collectSound:AudioClip; //在检测面板中设置其值function pickupCell(){ //临时申请一个AudioSource,播放完了即时销毁 AudioSource.PlayClipAtPoint(collectSound,transform.position);}

    6. 销毁物体

        Destroy(gameObject);  //即时销毁物体

        Destroy(gameObject,3.0); // 3秒之后销毁物体

    7. 给刚体增加速度

         方法一:

    #pragma strictvar throwsound:AudioClip;var coconut_bl:Rigidbody; //为预制物体function Update () { if(Input.GetButtonDown("Fire1")){  audio.PlayOneShot(throwsound);  var newcoconut:Rigidbody = Instantiate(coconut_bl,transform.position, transform.rotation);  newcoconut.velocity=transform.forward*30.0; //设置刚体速度 }}

    8. 忽视碰撞命令

       使用这两个碰撞器碰撞时不发生效果

       Physics.IgnoreCollision(transform.root.collider,newcoconut.collider,true); 


    9. 协同程序及动画播放

        协同程序:类似子线程,在运行本代码的同时,运动脚本中的其它代码。

    #pragma strict// this script will be attached to target objectprivate var beenhit:boolean = false;private var targetroot:Animation; var hitsound:AudioClip;var resetsound:AudioClip;var resettime:float=3.0;function Start () { targetroot = transform.parent.transform.parent.animation;}function OnCollisionEnter (theobject:Collision) { if(beenhit == false && theobject.gameObject.name == "coconut"){  StartCoroutine("targethit"); //协同程序:类似子线程,在运行本代码的同时,运动脚本中的其它代码 }}function targethit(){ audio.PlayOneShot(hitsound); // 播放声音 targetroot.Play("down");  // 播放动画 beenhit = trueyield new WaitForSeconds(resettime); // wait for 3s  audio.PlayOneShot(resetsound); targetroot.Play("up"); beenhit=false;}

    10. 动态添加和删除脚本

          

    #pragma strictvar obj:GameObject;var myskin:GUISkin;function Start () {}function Update () {}function OnGUI(){ //GUI.skin = myskin; if(GUILayout.Button("Add_Component",GUILayout.Height(40),GUILayout.Width(110))){  obj.AddComponent("xuanzhuan"); // xuanzhuan为一个脚本xuanzhuan。js } if(GUILayout.Button("Del_Component",GUILayout.Height(40),GUILayout.Width(110))){  var script:Object = obj.GetComponent("xuanzhuan");  Destroy(script); }  }

    11. 粒子系统

          粒子系统是在三维空间中渲染出来的二维图像,主要用于表现烟、火、水滴和落叶等效果。

          粒子系统由:粒子发射器、粒子动画器和粒子渲染器组成。


    12. for循环和数组

           使用Unity3D内建数组:

    var cms:Render[];fucntion Update(){   for(var co:Render in cms){      co.material.color = Color.red;   }}

           使用Unity3D非内建数组Array:

    var wenzi_bl:GUIText;var arr = new Array(9,12,65,58);  //静态数组var barr = new Array();  //动态数组fucntion Start(){   barr.Push("push1");   barr.Push("push2");   arr.Sort();   wenzi_bl.text = arr.join(",") + " ,arr长度:" + arr.length+ " >>" + barr + " barr长度:" + barr.length;}


    13. 延时函数及载入新场景

    function OnMouseDown () { audio.PlayOneShot(beep); yield new WaitForSeconds(0.35); Application.LoadLevel(levelToload);}


    14. 创建窗口及此窗口中的内容

    static var windowSwitch:boolean = false;private var windowRect = Rect(300,280,240,200);function Update(){ if(Input.GetKeyDown(KeyCode.Escape)){  windowSwitch = !windowSwitch; }}function OnGUI(){ if(windowSwitch){  windowRect = GUI.Window(0,windowRect,WindowContain,"测试窗口"); }}// show window contentsfunction WindowContain(){ if(GUI.Button(Rect(70,70,100,20),"关闭音乐")){  gameObject.Find("Terrain").GetComponent(AudioSource).enabled = false; }  if(GUI.Button(Rect(70,100,100,20),"播放音乐")){  gameObject.Find("Terrain").GetComponent(AudioSource).enabled = true; }  if(GUI.Button(Rect(70,130,100,20),"关闭窗口")){  windowSwitch = false; }  if(GUI.Button(Rect(70,160,100,20),"退出游戏")){  Application.Quit(); } GUI.DragWindow(new Rect(0,0,1000,1000));}

    15. 菜单制作方法

          1) GUITexture

          2) GUI对象:GUI.Window和GUISkin。


















               

    浏览人工智能教程

    展开全文
  • 最近对Unity3d进行学习的过程中,发现unity不仅入门教程做的丰富,而且Script API文档也是看过的所有API文档中最清晰易用的。不得不说,这极大降低了入门unity3d的门槛。 Unity3d入门学习资料   Unity3d的学习...

    最近对Unity3d进行学习的过程中,发现unity不仅入门教程做的丰富,而且Script API文档也是看过的所有API文档中最清晰易用的。不得不说,这极大降低了入门unity3d的门槛。

    Unity3d入门学习资料  

        Unity3d的学习首先从官方tutorials入手,如图1所示。



         Tutorial中最经典的demo便是Roll-A-Ball,直接看英文的视频讲解各种术语听起来有点头晕,有些东西也讲得太啰嗦。不过没关系,下面的这个unity圣典的中文教程绝对能满足你对unity开发入门的基本需求,作者逻辑清晰,讲解细致,不得不说入门视频首选:Unity5.2入门课程 - 进入Unity开发的奇幻世界

        每个tutorial中配套有相关的开发视频,并且能找到相关demo源码。在对unity3d的界面视窗和基本操作,以及对SceneGameObjectcomponent有基本了解之后,从demo源码入手学习是最快的,很容易加深对unity3d工程的理解,以及对Script API document的熟悉。


    Unity3d和Android的类比学习

        由于许多人有Android开发经验,Android与unity3d两者在设计上有诸多相似之处,下面以Android的角度来与unity3d进行类比学习,以加深对相关概念的理解。

    一、坐标系:




        Unity3d的三维坐标系,再加上不同的参考基准,相比Android的二维坐标系要复杂很多:

        1.在处理GameObjecttransform相关属性,如positionrotationscale时,假如直接用 鼠标放缩移动工具,三维坐标系拖动起来太难控制了。最方便精准的控制方式为两种:第一种是直接设定属性值;第二种便是shift+点击单个坐标轴,将三维坐标转到两位坐标进行控制。

        2.另外针对不同参考基准,unity坐标系划分为几种:

        1)世界坐标系:以描述所有场景内所有物体作为基准

        2) 局部坐标系:GameObject之间在位置上可构成父子关系,以父GameObject为基准

        3) 相机坐标系:相机是最终Game视角的真正观察者,以相机为基准,观察整个场景中    GameObject的立体位置前后和遮挡关系。

       4) 屏幕坐标系:很简单,就是把屏幕当作二维坐标,描述像素在屏幕中的位置。

        其中Transform提供了各坐标系相互转换的接口。

        3.向量和矩阵运算

        二维空间中,单纯对xy坐标进行操作,书写容易。但三维空间,向量和矩阵的运算形式的简洁性和高效性便体现出来。向量主要用于坐标轴的控制,矩阵对阵列数据的放缩、旋转和平移,以前学的《矩阵分析》理论便派上用场啦。 

    二、界面布局和控件的ID引用

     

    下面从unity3dGameObject引用和GameObject中的组件引用分别进行叙述。

    1.unity3dGameObject引用

        在界面布局元素中,Android叫控件,unity3dGameObject(游戏对象)。Android的的布局文件中定义了所需要的控件,在Java代码中通过查找id(findViewById)进行引用和控制。

        Unity3d的布局定义在scene文件中,并且在Hierarchy窗口中展现了所有GameObject列表。GameObject通过在inspector窗口中添加脚本组件来实现具体的逻辑控制。假如脚本中要对另外一个GameObject进行控制,比如roll-a-ball游戏中,球撞完所有立方体之后,需要触发一个文本UI的显示,显示为“You win!”,就需要球所在的游戏对象Player的脚本类中定义一个public变量: 

     public GUIText winText;

        随后在playerinspector面板中指定wintext对应的 GUI text对象,如图2所示。便可在脚本代码中实现修改GUIText对象的显示逻辑:winText.text = “You win!”     


        随后在playerinspector面板中指定wintext对应的 GUI text对象,如图2所示。便可在脚本代码中实现修改GUIText对象的显示逻辑:winText.text = “You win!”     

    2

       另一种引用的方法是设置为GameObject设置tag,通过GameObject.FindWithTag("tag")获取到需要引用的GameObject。下一段内容中会对此方法进行介绍。

    2.引用其他GameObjectComponent(组件)

        假如一个脚本组件想调用另一个GameObject的脚本组件中的public方法,怎么办呢?这种应用场景出现在roll-a-ball游戏中的球体撞击到立方体时,立方体需要调用球体绑定的脚本中的addScore函数,对分数显示GUI Text进行修改,实时更新分数。

        绑定到GameObject的脚本组件,本身是一个类文件,随着Scene的周期开始和结束,创建和销毁自己的实例。在整个内存空间中,应该是属于单例的,但实际上并不写成单例模式,那怎么获取其中的实例对象呢?

        其实原理很简单,直接通过GameObject来获取其绑定的脚本即可,下面便是具体方法:

        1) GameObject设置tag,一个tag便能分类一个或一类的GameObject,名字可以自定义,如图3所示。

    3

           2) 随后在脚本中通过tag获取到具体的GameObject

         GameObject gameControllerObject = GameObject.FindWithTag("pickup");
         if (gameControllerObject != null)
         {
             gameController = gameControllerObject.GetComponent <GameController>();
         } else {
             Debug.LogError("Cannot find 'GameController' script", transform);
         }

        3) 从步骤中获取的GameObject实例,获取其脚本组件,并对其Public方法进行调用:

       PlayController gameController;
       gameController = gameControllerObject.GetComponent <PlayController >();
       gameController.AddScore(scoreValue);
    

      三、生命周期

        AndroidServiceActivity都有自己的生命周期,也直接决定了不同函数的调用顺序。unity也是如此,unity脚本有自己固定的生命周期函数,常见函数如下:

       执行顺序为:awake>start>update

       更多函数的执行顺序如图4所示,欲知更多可参考这篇文章:Unity3D中自带事件函数的执行顺序

                                                     图4

    四、界面跳转

        Android一个完整的界面称之为Activity,通过startActivity来启动一个新的Activity实现跳转。但在Unity中称之为Scene(场景或关卡),假如只有一个scene,直接切换相机视角便可实现画面变化。但是如何实现不同scene的切换和跳转呢?

        在Scenes文件夹中创建不同的scene文件,File->Build Settings,可以看到图5界面。若创建的scene无法展现出来,首先确保scene文件是有内容的,然后点击“Add Open Scenes”,就可以实时加载进来。

                                                图5

        每一个scene文件都会对应一个level,要跳转到某个scene有两种方法:scene文件名和index level。下面是实现不同scene文件循环跳转的方法:

    using UnityEngine;
    using System.Collections;
    using UnityEngine.SceneManagement;
    public class SwitchScence : MonoBehaviour {
        public int scence_level;
        private int delay_cnt = 0;
           // Use this for initialization
           void Start () {
           }    
           // Update is called once per frame
           void Update () {
            delay_cnt ++;
            if (delay_cnt == 100) {
                SceneManager.LoadScene(scence_level);
                delay_cnt = 0;
            }       
        }
    }
    

         在Unity5.3中只要调用:SceneManager.LoadScene(scence_level),便可跳转到指定levelscene









    展开全文
  • http://www.gameres.com/654759.html 游戏程序 平台类型:   程序设计: 设计思想/框架 客户端  ...Unity3D/2D   GameRes游资网授权发布 文 / 吴秦
  • using UnityEngine; using System.Collections; using UnityEngine.UI; using UnityEngine.SceneManagement; public class LoadingScene : MonoBehaviour { public Slider processBar;... private AsyncOperat
  • Unity5的关卡切换

    2019-01-10 18:15:53
    本文章由cartzhang编写,转载请注明出处。 全部权利保留。 ...作者:cartzhang ...Unity 使用过程中关卡载入和卸载是大多数三维引擎都要提供的基本功能。 由于关卡切换在游戏中很经常使用。 在之前的版本号中Un...
  • 一、目录 【Unity3D从入门到进阶】文章目录及设置这个专栏的初衷
  • 应该所有的团队都会自己封装日志工具,除非引擎已经集成了,在Unity也不例外,当时之前的同事封装了一个有一个很大不爽的地方是:从Unity ConsoleWindow 双击日志跳转到代码总是跳转到封装类中的函数,而不能直接...
  • 作者:吴秦出处:http://www.cnblogs.com/skynet/本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是...虽然基于Unity3D,很多东西同样适用于Cocos。本文从以下10大点进行阐述: 架构设...
  • unity5.3.4场景切换 1.如果有两个场景点击开始游戏进入另一场景 2 、File--Build setting... 3、工程文件夹下创建脚本并把建好的C#脚本拖到 Maincarme或Canvas画布上都行。 ...
  • 虽然基于Unity3D,很多东西同样适用于Cocos。本文从以下10大点进行阐述: 1.架构设计 2.原生插件/平台交互 3.版本与补丁 4.用脚本,还是不用?这是一个问题 5.资源管理 6.性能优化 7.异常与...
  • 需求分析: 因项目需求,我们需要加载自定义的场景,这里的自定义是指,场景不会放在buildSetting里的场景,当然,不放在buildSetting里的场景,不算真的场景,只能是算半个,说白了就跟普通的assetBundle没啥区别...
1 2 3 4 5
收藏数 91
精华内容 36