Libgdx实现跨平台热更新

游戏开发中实现热更新可以实现无须重新打包,无须发布市场,无须等待审核,只需要将更新包放到服务器上,客户端就可以直接下载更新包来实现游戏的更新,在游戏后期的维护过程中,能为开发者提供十分的便利,正所谓工欲善其事,必先利其器。这篇文章就来说说如何在Libgdx中实现游戏的热更新。

原理

要实现游戏的热更新,首先必须对编译原理有一定的了解,不用掌握技术细节,但是基本流程是必须知道的。我们知道Libgdx的开发语言是Java,Java是一种静态语言,必须先编译成字节码才能在虚拟机中执行。我们正常开发的Java程序都会被编译成class文件,这个class文件就是字节码,程序执行的时候会由操作系统启动一个Java虚拟机,虚拟机再加载字节码,然后再去执行。所以要实现热更新,首先要实现的就是字节码的动态加载,好在Java为我们提供了ClassLoader类,这个类就是专门加载字节码的,虚拟机启动后会首先创建一个ClassLoader,加载程序中已经打包好的class文件,如果我们要加载其他的class,只需创建一个新的ClassLoader即可。当然其过程中需要注意的细节很多,待会儿再来细说。需要注意上面说的是在JAVA桌面程序中,如果是在Android,字节码是以dex为后缀名的文件,它是供Android中的虚拟机(Dalvik或Art)来加载执行的。那么在IOS上呢,IOS中并没有Java虚拟机,但是libgdx的跨平台解决方案使用了Multi-OS Engine( MOE ),来在IOS中运行Java,关于MOE的细节请自行查阅资料,这里只需知道Moe为我们在IOS中提供了一个Art虚拟机,它也是执行的dex文件。这样一来,同一份dex文件可以在android和ios上执行,简直不能再完美了。
理论说太多也没用,下面跟着一起做一个Libgdx的热更新Demo吧。

框架搭建

首先我们来搭建一个基于热更新的简单框架。

1.创建工程

使用Libgdx 1.9.5创建一个基本的libgdx工程,然后在Android Stuio中打开。这个工程包含了Desktop、Android、Ios-moe三个模块,当然还有必须的Core模块。

2.添加Game模块

用AndroidStudio为我们工程增加一个Java Library模块,命名为games,我们的游戏代码将放在这个模块里,以便将需要热更新的内容和主程序分开。Game模块创建好了之后,在它下面会自动生成一个build.gradle文件,我们需要在这里添加core模块的依赖:

dependencies {
    //添加core模块的依赖
    compile project(":core")
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

接下来还要在Game模块中新建一个assets文件夹,用来存放游戏的资源。注意android模块下也有一个assets文件夹,是用来存放主程序资源的。
因为只是demo,这里就把Game模块的代码一起写了吧。我新建了一个GameStage类,继承Stage,并添加了一张我网站的logo展示。注意这里的资源已经不是程序包里面的,而应该是外部资源了,所以需要从外部传入一个资源路径。

public class GameStage extends Stage {
    //需要主程序传入资源目录,传入MainGame以便能返回
    public GameStage(final MainGame mainGame, String assetsDir) {
        //添加一个logo图片,注意这里使用的是绝对路径
        Image logo = new Image(new Texture(Gdx.files.absolute(assetsDir + "logo.png")));
        logo.setPosition(getWidth() * 0.5f, getHeight() * 0.5f, Align.center);
        addActor(logo);
        //给logo添加事件
        logo.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                //点击将返回到mainStage
                mainGame.changeStage(mainGame.mainStage);
            }
        });
    }
}

这个时候,Game模块应该是这样的:
Game模块
这里的libs文件夹,也可以放一些Game模块需要的jar包,但是打包dex的时候,需要将jar一起打入,比较麻烦,还是建议jar包统一放到主程序中。

3.添加跨平台接口

因为android和ios实现热更新有些不同,而且core里面是无法使用dex的Loader的,所以需要定义一个跨平台的接口,然后在各个平台实现。在Core模块定义接口如下:

public interface HotUpdate {

    //获取游戏资源文件路径
    String getAssetsDir();

    //加载dex文件,并返回一个Class对象,若发成错误抛出异常
    Class loadDex(String dexPath, String className) throws Exception;
}

分别在Android和IOS模块下实现这个接口:

public class HotUpdateAndroid implements HotUpdate {
    private String assetsDir;//游戏资源文件目录
    private Context ctx;//AndroidContext

    public HotUpdateAndroid(Context ctx) {
        this.ctx = ctx;
        //Android的data目录,可以随apk卸载一起删除,并且资源文件的图片不会出现在相册中
        assetsDir = ctx.getExternalFilesDir("").getAbsolutePath() + "/";
    }

    @Override
    public String getAssetsDir() {
        return assetsDir;
    }

    @Override
    public Class loadDex(String dexPath, String className) throws Exception {
        DexClassLoader loader = new DexClassLoader(dexPath, ctx.getCacheDir().getAbsolutePath(), null, ctx.getClassLoader());
        Class claz = loader.loadClass(className);
        return claz;
    }
}
public class HotUpdateIos implements HotUpdate {

    @Override
    public String getAssetsDir() {
        //ios下资源存储目录,等效于 Gdx.files.getExternalStoragePath()
        return System.getenv("HOME") + "/Documents/";
    }

    @Override
    public Class loadDex(String dexPath, String className) throws Exception {
        //初始化一个PathClassLoader,加载dex文件
        PathClassLoader loader = new PathClassLoader(dexPath, getClass().getClassLoader());
        Class claz = loader.loadClass(className);
        return claz;
    }
}

稍微讲解一下PathClassLoader,它是android dalvik下的类,继承自Java的ClassLoader,可以用它来直接加载dex文件。和它一起的还有一个DexClassLoader,它也可以加载dex文件,同时它还能加载apk和jar中的dex。这里Android用的是DexClassLoader而IOS用的是PathClassLoader,其实都是可以的。另外不管是哪一个ClassLoader,其构造方法中有个参数是必不可少的,必须传入一个Parent ClassLoader,它的作用就是使新创建的Loader能够直接访问Parent Loader的类。
好了,接口定义完成,以后我们就只需要在core里面调用HotUpdate接口就能实现热更新的功能了。

MainGame类实现

在Core模块下创建一个MainGame类,并继承ApplicationAdapter。我在MainGame里添加了一个舞台mainStage,并添加了一张图片,点击图片就会调用HotUpdate的方法跳转到游戏界面了。因为HotUpdate只是一个接口,我们需要从各个平台传入它的实例,所以在MainGame的构造方法中传入一个参数。另外我还定义了一个切换舞台的方法,以便在游戏界面中也能返回到主界面。MainGame类代码如下:

public class MainGame extends ApplicationAdapter {
    public Stage currentStage, mainStage;
    HotUpdate hotUpdate;

    public MainGame(HotUpdate hotUpdate) {
        this.hotUpdate = hotUpdate;
    }

    @Override
    public void create() {
        //添加舞台,并添加图片
        mainStage = new Stage();
        Image img = new Image(new Texture("badlogic.jpg"));
        mainStage.addActor(img);
        changeStage(mainStage);
        //给图片添加监听
        img.addListener(new ClickListener() {
            @Override
            public void clicked(InputEvent event, float x, float y) {
                try {
                    //加载dex文件,并返回GameStage的Class对象
                    String className = "com.ayocrazy.tutorial.games.GameStage";//完整类名
                    Class claz = hotUpdate.loadDex(hotUpdate.getAssetsDir() + "game.dex", className);
                    //用Class对象初始化一个Stage
                    Stage gameStage = (Stage) claz.getConstructor(MainGame.class, String.class).newInstance(MainGame.this, hotUpdate.getAssetsDir());
                    //切换舞台,进入到gameStage
                    changeStage(gameStage);
                } catch (Exception e) {
                    //捕获异常
                    Gdx.app.log("loadDex error", e.toString());
                    e.printStackTrace();
                }
            }
        });
    }

    //切换舞台
    public void changeStage(Stage stage) {
        Gdx.input.setInputProcessor(stage);
        currentStage = stage;
    }

    @Override
    public void render() {
        Gdx.gl.glClearColor(0.9f, 0.9f, 0.9f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        if (currentStage != null) {
            currentStage.act();
            currentStage.draw();
        }
    }
}

最后,分别在AndroidLauncher和IOSLauncher两个类中,将HotUpdateAndroid和HotUpdateIOS的实例传入到MainGame。

public class AndroidLauncher extends AndroidApplication {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
        //传入HotUpdateAndroid的实例
        initialize(new MainGame(new HotUpdateAndroid(this)), config);
    }
}
public class IOSMoeLauncher extends IOSApplication.Delegate {

    protected IOSMoeLauncher(Pointer peer) {
        super(peer);
    }

    @Override
    protected IOSApplication createApplication() {
        IOSApplicationConfiguration config = new IOSApplicationConfiguration();
        config.useAccelerometer = false;
        //传入HotUpdateIOS的实例
        return new IOSApplication(new MainGame(new HotUpdateIos()), config);
    }

    public static void main(String[] argv) {
        UIKit.UIApplicationMain(0, null, null, IOSMoeLauncher.class.getName());
    }
}

至此,框架搭建已经完成,我们可以尝试运行一下看看。
我在ios上进入主程序正常,但是点击图片进入游戏界面无效果,打印了两句log:

System 5 ClassLoader referenced unknown path: /Users/ayo/Library/Developer/CoreSimulator/Devices/2DB6D358-14A6-4D64-A3DE-93E8DCFF7322/data/Containers/Data/Application/9FF34C72-7937-4604-97C4-E138783441AB/Documents/game.dex
loadDex error 4 java.lang.ClassNotFoundException: Didn't find class "GameStage" on path: DexPathList[[],nativeLibraryDirectories=[]]

第一句是ClassLoader报错,提示dex的路径不对,第二句是我捕获的异常,提示没有找到GameStage类。继续往下走。

生成dex

现在我们需要做的就是把游戏代码(这里只有GameStage类)生成dex文件,然后放到服务器上供主程序下载,这里作为演示,直接把生成的dex文件放到目标文件夹中。那么现在问题来了,如何生成dex文件了?原来Android SDK已经为我们提供了便利的工具,在Android SDK的build-tools目录下,有一个dx工具,它的作用是将jar或者class文件转换成dex,所以我们还需要将游戏代码先编译成class文件或者打包成jar,这就要用到jdk为我们提供的javac命令了。是不是好麻烦?限于篇幅,这里不去讨论这些工具的使用。但是我提供一个gradle脚本,可以实现直接将java文件打包成dex,其实原理上还是使用的javac和dx,只不过使用脚本要方便许多,写好一次,以后直接运行就能生成dex文件了。
继续吧!在Game模块下的build.gradle文件里空白处添加一个task,代码如下:

//添加类型为Exec的任务,并依赖java插件提供的classes任务
task packDex(dependsOn: classes, type: Exec) {
    //获取sdk的目录
    def sdkDir
    def btVersion = "25.0.0"//build-tools版本号,需要换成你自己的
    def localFile = file("../local.properties")
    if (localFile.exists()) {
        Properties localProp = new Properties()
        localFile.withInputStream { instr ->
            localProp.load(instr)
        }
        sdkDir = localProp.getProperty('sdk.dir')
        if (!sdkDir) {
            sdkDir = "$System.env.ANDROID_HOME"
        }
    }
    //dx工具路径,win下需改为dx.bat
    def dx = sdkDir + "/build-tools/$btVersion/dx"
    //要打包的class文件目录,classes任务默认会将java文件编译到这个目录
    def input = file("build/classes/main")
    //输出的dex文件目录
    def output = file("build/dex");
    //用于检测文件是否变动
    inputs.files input
    outputs.dir output
    //创建目录
    file(output).mkdirs();
    //执行命令行
    commandLine "$dx", '--dex', "--output=$output/game.dex", input
}

稍微阐述一下,classes是Java Gradle插件的内置任务,会将Java代码打包成class文件,并放到build/classes/main路径下,然后我们写的这个packDex任务将该路径下的class文件打包成dex,放到build/dex路径下。使用方法是先刷新Gradle,然后在Android Studio的右侧找到Gradle的任务列表,在games/other里能找到我们定义的packDex任务,双击执行就可以了。如果你有配置Gradle的环境变量,也可以直接在工程目录下输入gradle packDex命令来打包,更加快捷。打包成功后,就能在Game模块下的build/dex目录看到dex文件了。此时的Game模块如图:
dex文件

打包上传

dex生成好了,还差资源文件了。如果是在正式环境,可以将dex和Game模块下的assets文件夹一起打包成zip包,上传到服务器,然后由主程序下载并解压。并且,我们同样可以使用Gradle脚本任务来进行打包操作,十分方便。
这里作为Demo,我直接将dex和资源文件复制到了ios模拟器里。

加载运行

在运行之前还有一个至关重要的事情要做,因为我们的GameStage类引用了MainGame类,然而moe在打包的时候默认是开启混淆的,所以MainGame类在主程序中被混淆了而GameStage类里的MainGame类是没被混淆的,实际运行会报错。所以我们还需要关闭MainGame类的混淆,这里我直接关闭了com.ayocrazy.toturial包的混淆,在ios-moe模块下的proguard.append.cfg文件中加入:

-keep class com.badlogic.** { *; }
-keep enum com.badlogic.** { *; }
-keep class com.ayocrazy.tutorial.** { *; }

好了,激动人心的时刻到了,赶快运行起来看看效果吧:
运行效果图
在上图中,第一次点击后我将GameStage的代码改了,然后重新打包成dex放入到模拟器里覆盖之前的文件,再次进入的时候已经是新的代码了,可以看到我给logo添加了一个动画,完美实现热更新。
Android下效果是一样的,已经验证,这里不截图了,可以自己运行查看。

效率

关于libgdx的热更新到这里其实已经算是讲完了,但是用这种方式投入生产开发的话效率很低,所以下面讨论下如何提升开发效率:

  1. 调试。首先dex的内容是没法实现断点调试的,ide不支持,如果你足够强大可以自己写debug工具。但是我们也可以用桌面项目来进行调试,让桌面项目运行的时候将Game模块的内容包含进去,运行desktop模块的时候就不用dex了,也就不存在热更新,况且本来desktop也是没法加载dex的。
  2. 资源。我们使用AssetManager管理资源的时候需要特别注意,在主程序中使用的是internal路径,而在游戏中是absolute路径,所以无法使用同一个AssetManager来管理,只能分别管理。这个时候就要特别注意主程序和游戏的生命周期,小心处理内存问题。
  3. 工具。这里主要指Gradle,擅用Gradle可以让你的开发效率大大提升。除了前面说的用gradle生成dex,你还可以用它来打zip包,上传服务器,复制文件到Android手机、IOS模拟器都是可以的,包括第1点里提到的让desktop运行的时候包含Game模块的内容也是可以使用Gradle脚本实现的。当然这里只提供思路,细节还需自行研究,可以告诉大家的是这些功能在我的项目中都已经实现。如果有疑问,可以留言一起探讨。
  4. 架构。良好的架构可以让程序的稳定性、可维护性大大增强。给游戏添加热更新会提高开发的复杂度,如果没有一个良好的架构支持,必然导致开发过程中痛苦万分,那样就本末倒置了。

最后,作个总结吧。Libgdx实现跨平台的热更新可以归纳为四个步骤:模块分离->代码编写->dex生成->热更加载。 技术上实现并不难,难的它打破了原有的开发流程,在给我们带来强大功能的同时,也失去了一些便利性,所以也不要盲目追求热更新,还是面向需求编程吧。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
 更新框架设计系列课程总体介绍:       本系列课程由《更新框架设计之Xlua基础》、《更新框架设计之更流程与补丁技术》、《更新框架设计之游戏客户端框架》三套课程组成。 三套课程是一个不可分割有机的整体,笔者带领大家由浅入深逐级深入 ,在领悟更精髓的基础之上,通过高端架构设计,**完成设计出“低耦合”、“低侵入”、“高复用”性的游戏(VR/AR)客户端更框架。《更新框架设计之客户端更框架》课程介绍:       本作为更框架系列课程中的客户端框架设计与实现部分。理解本作需要之前的所有知识点积累,在其基础之上给学员展现当今商业更框架中,商业级更框架的基本原理、设计全过程、实现框架产品等全过程。通过本作学习可以让资深开发人员晋升为游戏架构师、主程、技术总监等职位。        为了更好更快的服务广大学员,本课程分为上、中、下三部分,内容如下:        上部:             UI框架与AB框架整合,重构整合为 “更新UI框架”。        中部:             “更新UI框架”与更流程技术重构整合。               纯Lua框架设计理念与实现。        下部:              复合型更框架设计与实现。              框架产品加入HotFix功能模块,且功能演示与测试完善。  A:《更新框架设计之客户端更框架(中)》https://edu.csdn.net/course/detail/27135B:《更新框架设计之客户端更框架(下)》https://edu.csdn.net/course/detail/27136 

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值