Libgdx实现跨平台热更新

    xiaoxiao2026-02-24  7

    游戏开发中实现热更新可以实现无须重新打包,无须发布市场,无须等待审核,只需要将更新包放到服务器上,客户端就可以直接下载更新包来实现游戏的更新,在游戏后期的维护过程中,能为开发者提供十分的便利,正所谓工欲善其事,必先利其器。这篇文章就来说说如何在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模块应该是这样的:这里的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和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的热更新到这里其实已经算是讲完了,但是用这种方式投入生产开发的话效率很低,所以下面讨论下如何提升开发效率:

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

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

    相关资源:python入门教程(PDF版)
    最新回复(0)