Unity快速切换平台

不同平台下的Unity工程可以公用一个Asset,只需要为不同平台缓存Library就可以,只要我们通过symbolic link的方式来完成

symbolic link


Windows平台

Mac平台

Unity资源打包

什么是AssetBundle


  • AssetBundle是一个压缩包包含模型、贴图、预制体、声音、场景等资源,在游戏运行过程中被加载
  • 这个压缩包可以认为是一个文件夹,里面包含了多个文件。这些文件可以分为两类:serialized file 和 resource files(序列化文件和源文件)serialized file:资源被打碎放在一个对象中,最后统一被写进一个单独的文件(只有一个);resource files:某些二进制资源(图片、声音)被单独保存,方便快速加载

AssetBundle的主要作用


  • 游戏运行过程中可以被加载
  • AssetBundle自身保存着互相的依赖关系
  • 压缩包可以使用LZMA和LZ4压缩算法,减少包大小,更快的进行网络传输
  • 把一些可以下载内容放在AssetBundle里面,可以减少安装包的大小

AssetBundle打包


如下图所示,我们针对资源在Asset Labels中设置Asset Bundle名字

file

下面我们开始构建AssetBundle包

  • 创建一个文件夹命名Editor,创建一个编辑器扩展类CreateAssetbundles
using UnityEditor;
using System.IO;

public class CreateAssetbundles  {

    [MenuItem("AssetsBundle/Build AssetBundles")]
     static void BuildAllAssetBundles()//进行打包
    {
        string dir = "AssetBundles";
        // 判断该目录是否存在
        if (Directory.Exists(dir) == false)
        {
            // 在工程下创建AssetBundles目录
            Directory.CreateDirectory(dir);
        }
        // 参数一为打包到哪个路径,参数二压缩选项  参数三 平台的目标
        BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None,BuildTarget.StandaloneWindows64);
    }
}
BuildAssetBundleOptions.None:使用LZMA算法压缩,压缩的包更小,但是加载时间更长。使用之前需要整体解压。一旦被解压,这个包会使用LZ4重新压缩。使用资源的时候不需要整体解压。在下载的时候可以使用LZMA算法,一旦它被下载了之后,它会使用LZ4算法保存到本地上。
BuildAssetBundleOptions.UncompressedAssetBundle:不压缩,包大,加载快
BuildAssetBundleOptions.ChunkBasedCompression:使用LZ4压缩,压缩率没有LZMA高,但是我们可以加载指定资源而不用解压全部。
注意使用LZ4压缩,可以获得可以跟不压缩想媲美的加载速度,而且比不压缩文件要小。

所以一般我们选择LZ4的打包压缩方式

AssetBundle加载


  • 从内存加载使用LoadFromMemoryAsync
  • 从本地文件加载可以使用LoadFromFile
  • 从服务器上Web上加载可以使用UnityWbRequest

我们根据上面所给的三种方式,分别进行说明

LoadFromMemoryAsync方式

using UnityEngine;
using System.IO;
using System.Collections;
public class LoadFromFileExample : MonoBehaviour {

    IEnumerator Start () {
        string path = "AssetBundles/iOS/common_icons";
        // 第一种加载AB的方式 LoadFromMemoryAsync
        // 异步加载
        AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path));
        yield return request;
        AssetBundle ab = request.assetBundle;
        // 同步方式
        // AssetBundle ab =  AssetBundle.LoadFromMemory(File.ReadAllBytes(path));

         // 使用里面的资源
        Object[] obj = ab.LoadAllAssets<GameObject>();//加载出来放入数组中
        // 创建出来
        foreach (Object o in obj)
        {
            Instantiate(o);
        }
    }
}

LoadFromFile方式

using UnityEngine;
using System.Collections;

public class LoadFromFileExample : MonoBehaviour {

    IEnumerator Start () {
        string path = "AssetBundles/iOS/common_icons";
        // 第二种加载方式 LoadFromFile
        // 异步加载
        AssetBundleCreateRequest request = AssetBundle.LoadFromFileAsync(path);
        yield return request;
        AssetBundle ab = request.assetBundle;
        // 同步加载
        // AssetBundle ab = AssetBundle.LoadFromFile(path);

        // 使用里面的资源, 加载出来放入数组中
        Object[] obj = ab.LoadAllAssets<GameObject>();
        // 创建出来
        foreach (Object o in obj)
        {
            Instantiate(o);
        }
     }
}

UnityWbRequest方式

using UnityEngine;
using System.Collections;
using UnityEngine.Networking;

public class LoadFromFileExample : MonoBehaviour {

    IEnumerator Start () {
        // 第三种加载方式使用UnityWbRequest服务器加载使用http本地加载或远程加载使用file
        string uri = @"http://127.0.0.1:8080/AssetBundles/iOS/common_icons";
        UnityWebRequest request = UnityWebRequest.GetAssetBundle(uri);
        yield return request.Send();
        AssetBundle ab = DownloadHandlerAssetBundle.GetContent(request);
        // 使用里面的资源, 加载出来放入数组中
        Object[] obj = ab.LoadAllAssets<GameObject>();
        // 创建出来
        foreach (Object o in obj)
        {
            Instantiate(o);
        }
    }
}

AssetBundle分组策略


  • 把经常更新的资源放在一个单独的包里面,跟不经常更新的包分离
  • 把需要同时加载的资源放在一个包里面
  • 可以把其他包共享的资源放在一个单独的包里面
  • 把一些需要同时加载的小资源打包成一个包
  • 如果对于一个同一个资源有两个版本,可以考虑通过后缀来区分

AssetBundle中的依赖


什么叫做依赖呢?比如我们两个预设(Prefab)需要用到同样的纹理(Texture),那么Prefab和Texture之间就存在着依赖关系,如果我们对两个预设分别打包,Texture就会存在两份,包体就会变大,我们该如何解决这个问题呢?

  • 首先将用到的同样的纹理图打包成AssetBundle
  • 然后分别打包两个预设

这样就可以缩减打包出来的体积大小
此时我们再加载AssetBundle中的Prefab,同时需要将他们依赖的纹理也加载到内存中,否则会出现纹理丢失的情况。如下所示

using UnityEngine;
public class LoadFromFileExample : MonoBehaviour {
    void Start () {
        AssetBundle ab = AssetBundle.LoadFromFile("AssetBundles/iOS/Prefab1");
        AssetBundle textureAB = AssetBundle.LoadFromFile("AssetBundles/iOS/texture");
        // GameObject go = ab.LoadAsset<GameObject>("Prefab1");
        // Instantiate(go);
        Object[] obj = ab.LoadAllAssets<GameObject>();
        // 创建出来
        foreach (Object o in obj)
        {
            Instantiate(o);
        }
    }
}

在打包完AssetBundle之后会出现AssetBundlesAssetBundles.manifest两个文件,所有打包出来的AssetBundle都会放在其中;所以我们可以读取这两个文件,从而达到获取所有AssetBundle的目的。

AssetBundle manifesAB = AssetBundle.LoadFromFile("AssetBundles/iOS/AssetBundles");
AssetBundleManifest manifest= manifesAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
foreach (string name in manifest.GetAllAssetBundles())
{
    print(name);
}

然后我们利用manifest文件加载某个AssetBundle的依赖

...
AssetBundle manifesAB = AssetBundle.LoadFromFile("AssetBundles/AssetBundles");
AssetBundleManifest manifest= manifesAB.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
foreach (string name in manifest.GetAllAssetBundles())
{
    print(name);
}
string []strs = manifest.GetAllDependencies("Prefab1");
foreach (var name in strs)
{
    AssetBundle.LoadFromFile("AssetBundles/" + name);
}
...

AssetBundle的卸载

一般我们使用AssetBundle.Unload对资源进行卸载工作

  • AssetBundle.Unload(true) 卸载所有资源,即使有资源被使用着
  • AssetBundle.Unload(false)卸载所有没用被使用的资源
  • Resources.UnloadUnusedAssets

文件校验

文件校验可以在文件传输的时候保证文件的完整性,例如A在给我传输了一个文件之前会生成一个校验码,对于这个文件只会生成这一个唯一的校验码,只要传输给我的文件有一点不一样那么校验码就会完全不同。所以A在传输给我文件的时候会把文件和校验码都传输给我,当我取到这个文件的时候我也会使用和A同样一个算法去生成这个文件的校验码,然后拿这个值和A传输给我的校验码比对,如果一样说明这个文件是完整的,如果不一样那么就重新传输。

CRC MD5 SHA1几种方式对比

  • 算法不同。CRC采用多项式除法,MD5和SHA1使用的是替换、轮转等方法
  • 校验值的长度不同。CRC校验位的长度跟其多项式有关系,一般为16位或32位;MD5是16个字节(128位);SHA1是20个字节(160位)
  • 校验值的称呼不同。CRC一般叫做CRC值;MD5和SHA1一般叫做哈希值(Hash)或散列值
  • 安全性不同
  • 效率不同,CRC的计算效率很高;MD5和SHA1比较慢
  • 用途不同。CRC一般用作通信数据的校验;MD5和SHA1用于安全(Security)领域

Unity Asset Bundle Browser Tool

下载地址:
https://github.com/Unity-Technologies/AssetBundles-Browser

参考资料

https://www.jianshu.com/p/5d4145cd900c

Unity开发框架整理

ToLua

此Unity框架支持Lua,所使用lua为ToLua,这里介绍了有关uLua以及toLua的相关内容。当然除了uLua和toLua意外,还有腾讯开发的XLua,xLua更加灵活,支持直接给C#进行打补丁的操作。
使用toLua开发的游戏很多,如下图所示



这里面包括了最火的王者荣耀
https://github.com/topameng/CsToLua

Unity


一般成熟项目的Unity工程至少包含
Editor
Plugins
Resources
StreamingAssets
这几个目录
然后框架中Game作为C#核心库

开发框架

  • 主入口Main
  • GameMain作为单例一直存在与整个游戏的生命周期内,主要完成初始化工作。

  • 基本类关系
  • Facade
    作为模块管理器存在,主要作用完成模块的初始化添加、获取与删除。
    主要接口有AddModule、GetModule、RemoveModule,而我们的框架使用的是继承于Facade的AppFacade。
    在Main中除了初始化AppFacade还有就是初始化状态机。

  • GameStateMachine
  • GameStateMachine stateMachine = AppFacade.Instance.AddModule ();
    

    状态机通过调用StateTransition方法来完成游戏状态的转变以及执行相应的代码

    public T StateTransition () where T : GameStateBase {
        GameStateBase oldState = currentState;
        currentState = gameObject.AddComponent ();
        if (oldState != null)
            oldState.PrepareExit ();
    
        currentState.PrepareEnter ();
        if (oldState != null)
            oldState.Exit ();
    
        currentState.Enter ();
        if (oldState != null)
            oldState.DoneExit ();
    
        currentState.DoneEnter ();
        Object.Destroy (oldState);      // 销毁旧的状态(组件)
        return currentState as T;
    }
    

    大致的流程分
    1 旧状态->PrepareExit
    2 旧状态->Exit
    3 新状态->Enter
    4 旧状态->DoneExit
    5 新状态->DoneExit

    状态机初始化完成操作:
    <状态:Initializing>
    1 添加FileHelper模块,主要用于文件操作。同时配置文件搜索路径。
    2 添加AssetBundleLoader模块,主要用于AssetBundle的相关操作。
    3 状态转变为Upgrading。
    <状态:Upgrading>
    1 添加ResourceDownloader模块,用于资源和代码下载。
    2 完成Upgrade工作。
    3 初始化AssetBundleLoader。
    4 添加并初始化ResourceManager,主要用于资源管理。
    5 添加并初始化LuaManager,主要用于Lua管理。
    Lua初始化调用

    protected virtual void OnLoadFinished()
    {
        luaState.Start();
        StartLooper();
        StartMain();
    }
    
    protected virtual void StartMain()
    {
        luaState.DoFile("Main.lua");
        levelLoaded = luaState.GetFunction("OnLevelWasLoaded");
        CallMain();
    }
    

    此时已经运行到Main.lua,前提一定完成Lua Binding。
    6 状态转变为Ready。
    此时完成了框架初始化的工作。这里需要特别注意的是在执行MainLua之前,我们需要加载完成需要使用的Lua,通过接口:DoLoadAssetBundleAsync。

    IEnumerator DoLoadAssetBundleAsync (Action onFinish) {
        // Get lua manifest
        string path = GameSettings.AppContentPath + GameSettings.GetOS () + "/" + GameConsts.luaBundleFile;
        string json = File.ReadAllText (path);
    
        LuaBundleList manifest = JsonUtility.FromJson (json);
    
        for (int i = 0; i < manifest.items.Count; i++) {
            string abName = manifest.items [i];
    
            yield return StartCoroutine (DoLoadAssetBundleAsync (abName, delegate (AssetBundle ab) {
                if (!ab) {
                    Debug.Log ("[LuaManager] DoLoadAssetBundleAsync : Can't find the assetbundle " + abName);
                    return;
                }
    
                // Add the bundle to searched bundles.
                abName = abName.Replace ("lua/", "");
                abName = abName.Replace (GameSettings.ExtName, "");
                LuaFileUtils.Instance.AddSearchBundle (abName.ToLower (), ab);
            }));
        }
            
        onFinish.Invoke ();
    }
    
    IEnumerator DoLoadAssetBundleAsync (string abName, Action onFinish) {
        string abPath = FileHelper.Instance.FindFile (abName);
        if (abPath == null) {
            onFinish (null);
            yield break;
        }
    
        AssetBundle ab = null;
        if (GameSettings.LuaEncryptMode) {
            abPath = "file://" + abPath;
    
            WWW www = new WWW (abPath);
            yield return www;
    
            byte[] encData = www.bytes;
            byte[] decData = CryptoUtil.Decrypt (encData, GameSettings.Secret);
    
            var loadRequest = AssetBundle.LoadFromMemoryAsync (decData);
            yield return loadRequest;
    
            ab = loadRequest.assetBundle;
        } else {
            var loadRequest = AssetBundle.LoadFromFileAsync (abPath);
            yield return loadRequest;
    
            ab = loadRequest.assetBundle;
        }
    
        onFinish (ab);
    }
    

    故此我们知道LuaBundles.json是我们需要初始化的luaBundle。

  • Lua
  • CustomSettings是我们需要绑定C#接口给Lua调用定义的地方。
    Main.Lua是LuaManager会初始化调用的文件,而Main函数是最开始调用的。

    require "init"
    ...
    local app = nil
    function Main()
        // 这里GameMain是游戏整个生命周期的都存在的
        local gameMain = UGameObject.Find("GameMain")
        app = App:instance(gameMain)
        app:registerScene(MainScene())
        app:registerScene(DinningScene())
        app:registerScene(LoadScene())
        app:registerTransition(MaskTransition())
        app:runScene(SceneConsts.Main)
    end
    

    init主要完成了lua脚本以及运行框架的所有初始化,大致完成了如下功能定义和初始化:
    1 async(Lua异步接口)
    async.iterator = function(fn_list)
    async.waterfall = function(fn_list, cb)
    async.forEach = function(arr, it, callback)
    async.forEachSeries = function(arr, it, callback)
    参考:Github Async

    2 class(类定义)

    function clone(object)
        local lookup_table = { }
        local function _copy(object)
            if type(object) ~= "table" then
                return object
            elseif lookup_table[object] then
                return lookup_table[object]
            end
            local new_table = { }
            lookup_table[object] = new_table
            for key, value in pairs(object) do
                new_table[_copy(key)] = _copy(value)
            end
            return setmetatable(new_table, getmetatable(object))
        end
        return _copy(object)
    end
    function class(base)
        local c = { }
        if type(base) == 'table' then
            -- our new class is a shallow copy of the base class!
            for i, v in pairs(base) do
                c[i] = v
            end
            c._base = base
        end
        -- the class will be the metatable for all its objects,
        -- and they will look up their methods in it.
        c.__index = c
    
        -- expose a constructor which can be called by ()
        local mt = { }
        mt.__call = function(class_tbl, ...)
            local obj = { }
            setmetatable(obj, c)
            if class_tbl.init then
                class_tbl.init(obj, ...)
            else
                -- make sure that any stuff from the base class is initialized!
                if base and base.init then
                    base.init(obj, ...)
                end
            end
            return obj
        end
        c.is_a = function(self, klass)
            local m = getmetatable(self)
            while m do
                if m == klass then return true end
                m = m._base
            end
            return false
        end
        setmetatable(c, mt)
        return c
    end
    -- A = class()
    -- function A:init(x)
    --     self.x = x
    -- end
    -- function A:test()
    --     print(self.x)
    -- end
    
    -- B = class(A)
    -- function B:init(x, y)
    --     A.init(self, x)
    --     self.y = y
    -- end
    
    -- b = B(1, 2)
    -- b:test()
    
    -- print(b:is_a(A))
    

    3 function(公用函数合集)
    4 const常量合集,用于MVC分层的常量定义。
    5 Utility
    6 Core主要包括App、Controller、Scene、View以及消息机制和管理器
    7 game中主要是逻辑代码和逻辑的MVC分层

    Unity – IOS与Unity交互

  • Unity部署IOS配置简况

  • 接口信息
  • BlueToothManagerInterface.h

    #import 
    @interface BlueToothManagerInterface : NSObject
    @end
    

    BlueToothManagerInterface.mm

    #import "BlueToothManagerInterface.h"
    #import "BlueToothManager.h"
    extern "C"
    {
        //初始化
        void InitBlueToothManager(){
            // TODO
        }
    
        // 运行
        void RunBlue() {
            [[BlueToothManager shareInstance] RunBlue];
        }
    }
    

    将上述文件放置Unity的Plugins/iOS中

  • Unity接口信息
  • using UnityEngine;
    using System.Collections;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using LuaInterface;
    public class IOSManager {
        private static IOSManager sInstance = null;
        public static IOSManager getInstance() {
            if (sInstance == null) {
                sInstance = new IOSManager ();
            }
            return sInstance;
        }
        #region
        // 注意是两个下划线
        [DllImport("__Internal")]
        private static extern void RunBlue ();
        /// 
        /// 开起蓝牙
        /// 
        public void RunBlueNative() {
            #if UNITY_IOS && !UNITY_EDITOR
            RunBlue();
            #endif
        }
        #endregion
        // 上面即完成了Unity调用iOS的过程
    }
    

    下面我们看下iOS如何调用Unity.
    这里和Android类似,主要利用Unity的UnitySendMessage函数

  • UnitySendMessage接口
  • 同理其中参数1是场景中接受消息的对象,参数2是要执行的函数名,参数3为传入参数.
    具体参考如下,我们在iOS蓝牙模块中定义

    // .h
    /**
     * iOS To Unity
     */
    - (void) testUnity;
    // .m
    - (void) testUnity {
        UnitySendMessage("ScriptObject", "setString", "Test...");
    }
    

    上述经过测试没有问题

    Unity3d中SendMessage – Android和Unity的交互

      • UnityPlayer的SendMessage

    SendMessage

    
    // paramString1 表示挂载脚本的对象
    // paramString2 表示脚本中调用的方法名
    // paramString3 传参数
    UnityPlayer.UnitySendMessage(paramString1, paramString2, paramString3);
    
      AndroidJavaClass和AndroidJavaObject
    
    using UnityEngine;
    using System.Collections;
    public static class AndroidUtils {
    	public static AndroidJavaClass unityOverrideClass;
    	public static AndroidJavaClass UnityOverrideClass {
    		get {
    			if (unityOverrideClass == null) {
    				unityOverrideClass = new AndroidJavaClass ("com.xxx.xxxx.xxxxActivity");
    			}
    			return unityOverrideClass;
    		}
    	}
    
    	public static AndroidJavaObject SDK {
    		get {
    			return UnityOverrideClass.CallStatic ("GetSDK", new object[0]);
    		}
    	}
    }
    

    同时需要我们Android封装Jar包,以及AndroidManifest.xml,XML中定义的Activity所需要的res也需要。将这些内容放置到Plugins中:

    Unity调用Android的方法如下

    
    AndroidUtils.SDK.Call("AndroidMethodName", args);
    

    AndroidManifest.xml示例

    <?xml version="1.0" encoding="utf-8" standalone="no"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto" package="com.xx.xxxx" platformBuildVersionCode="22" platformBuildVersionName="5.1.1-1819727">
     <supports-screens android:largeScreens="true" android:normalScreens="false" android:requiresSmallestWidthDp="600" android:smallScreens="false" android:xlargeScreens="true"/>
     <uses-feature android:glEsVersion="0x00020000"/>
     <supports-gl-texture android:name="GL_OES_compressed_ETC1_RGB8_texture"/>
     <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
     <uses-feature android:name="android.hardware.touchscreen.multitouch" android:required="false"/>
     <uses-feature android:name="android.hardware.touchscreen.multitouch.distinct" android:required="false"/>
     <uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
     <uses-permission android:name="android.permission.INTERNET"/>
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
     <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
     <uses-permission android:name="android.permission.BLUETOOTH"/>
     <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
     <uses-permission android:name="android.permission.CAMERA"/>
     <android:uses-permission android:name="android.permission.READ_PHONE_STATE"/>
     <application android:allowBackup="true" android:icon="@drawable/app_icon" android:isGame="true" android:label="@string/app_name" android:largeHeap="true" android:name="com.xxx.xxxx.BLApplication" android:supportsRtl="true" android:theme="@style/UnityThemeSelector">
     <activity android:configChanges="locale|fontScale|keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode" android:exported="true" android:label="@string/app_name" android:launchMode="singleTask" android:name="com.xx.xxx.SDKNativeActivity" android:screenOrientation="sensorLandscape">
     <meta-data android:name="unityplayer.UnityActivity" android:value="true"/>
     <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="false"/>
     <intent-filter>
     <action android:name="android.intent.action.MAIN"/>
     <category android:name="android.intent.category.LAUNCHER"/>
     </intent-filter>
     </activity>
     </application>
    </manifest>