分类: libGDX

  • libGDX快速集成lua脚本

    libGDX快速集成lua脚本

    Lua确实是一个非常常用的脚本,很多时候处于快速更新应用或者游戏AI自身所需,我们必须在游戏中集成脚本支持。而Lua是一个不错的选择。

    Lua语言规范精简、运行时库小,非常容易创建受控环境,安全性也不错。虽然是一个小巧的脚本语言,但是支持协程,能够在关键时刻发挥很大作用。

    要在libGDX中集成Lua非常简单,只需要选择一款java实现的Lua运行环境即可。本文以luaj为例。

    首先在build.gradle中添加依赖

    compile "org.luaj:luaj-jse:3.0.1"

    然后在选择需要调用的地方,这里来个简单的例子,我们需要在stage初始化时添加一个Image对象,而Image对象的资源来自TextureAtlas类,为了演示的完整性,我们还会把Lua脚本执行结果赋值给Java上下文。

    这是原有的Java代码

    atlas = DartsGame.getManager().get("pack/sha/default.pack", TextureAtlas.class); // 获取图册
    man = new Image(atlas.findRegion(PLAYER)); // 获取图册中的Player.png并创建image对象
    man.setName("player");
    man.setX(0);
    man.setY(160 - man.getHeight() / 2); // 设置Y值,以让图片在中间显示
    stage.addActor(man); // 将主角添加到舞台
    

    新创建一个Lua脚本

    function man(stage, atlas)
        man = luajava.newInstance("com.badlogic.gdx.scenes.scene2d.ui.Image", atlas:findRegion("Player"))
        man:setName("player")
        man:setX(0)
        man:setY(160 - man:getHeight() / 2)
        stage:addActor(man)
        return man
    end

    这里我们return man是为了演示Lua脚本和Java宿主的交互,并没有太多实际的意义。

    然后返回Java代码,一般我们会在一些常见的地方添加脚本调用点,但是对应的调用可以为空,一般我会选择Stage初始化的时候,Stage update的时候。然后会传入一些常见的Java对象给予Lua脚本,常见的包括stage和assetManager。

    Globals globals = JsePlatform.standardGlobals();
    LuaValue load = globals.load(Gdx.files.local("man.lua").readString());
    load.call();
    Varargs varargs = globals.get("man").invoke(new LuaValue[] {
        CoerceJavaToLua.coerce(stage),
        CoerceJavaToLua.coerce(atlas)
    });
    man = (Image) CoerceLuaToJava.coerce(varargs.arg1(), Image.class);

    这里需要注意的是CoerceJavaToLua和CoerceLuaToJava两个类,它们分别提供了Java对象转Lua对象和Lua对象转Java对象的功能。因为Lua默认支持的类型非常少,所以大部分时候LuaValue.of是不够用的。这两个类其实使用了userdatas作为传递对象,直接将Java的对象放入userdatas中,并保持class的信息。

    public Object coerce(LuaValue value) {
        switch (value.type()) {
        case LuaValue.TNUMBER:
            return value.isint() ? (Object) new Integer(value.toint()) : (Object) new Double(value.todouble());
        case LuaValue.TBOOLEAN:
            return value.toboolean() ? Boolean.TRUE: Boolean.FALSE;
        case LuaValue.TSTRING:
            return value.tojstring();
        case LuaValue.TUSERDATA:
            return value.optuserdata(targetType, null);
        case LuaValue.TNIL:
            return null;
        default:
            return value;
        }
    }

    因为coerce也支持普通Lua类型,所以可以放心使用。

    另外Intellij有Lua脚本插件,所以写起来还好

  • 在Travis中集成libGDX项目

    之前Github放了一个例子,版本很老,是0.9.8的,后面1.x时代有一天突发奇想又去升级了一下,升级到了1.5.6。

    今天突然有人又问到这个了,就顺手升级到1.9.5。测试了一下桌面环境是工作的,不过android环境不太想测试了,就寻思着集成一下Travis来测试一下,因为项目本身没有单元测试,所以测试的要求也不高,一是能编译通过,生成apk包,二是能够apk能够安装。

    Travis是自带了android支持的,不过还是beta版,说实话beta版果然坑多。

    首先来个简单的.travis.yml 文件

    language: android
    
    android:
      components:
        - build-tools-23.0.1
        - android-24

    然后报错说找不到android-24。马上检查了一下本地的android sdk,有这个版本号。

    然后网上找了一下方案,原来默认最高支持就到23,要支持高版本需要这样

    language: android
    
    android:
      components:
        - tools
        - tools
        - build-tools-23.0.1
        - android-24

    这个的tools必须重复两遍,一次拉取最新的xml,一次才是真正的安装。

    这样之后可以生成apk了,再来试试安装apk吧。

    思路很简单:创建一个模拟器,然后启动,最后安装即可。Travis自带了一个命令android-wait-for-emulator 来等待模拟器启动。

    在components里面声明– sys-img-armeabi-v7a-android-24 然后添加

    after_script:
        - echo no | android create avd --force -n test -t android-24 --abi armeabi-v7a
        - emulator -avd test -no-audio -no-window &
        - android-wait-for-emulator
        - adb shell input keyevent 82 &
        - adb devices
        - adb install -r android/build/apk/android-release-unsigned.apk

    然而调用Travis自带的等待命令的时候报错qemu-system-armel: -audio: invalid option 。

    只好换回老版本,22来启动模拟器

    after_script:
        - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a
        - emulator -avd test -no-audio -no-window &
        - android-wait-for-emulator
        - adb shell input keyevent 82 &
        - adb devices
        - adb install -r android/build/apk/android-release-unsigned.apk

    然后,它就卡着不动了。一直是等待启动中…

    仔细检查日志,还是版本问题,没有安装API 22,所以镜像没有安装成功,实在没法,还是放弃24,使用22编译即可。

    language: android
    
    android:
      components:
        - build-tools-23.0.1
        - android-22
        - sys-img-armeabi-v7a-android-22
    after_script:
        - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a
        - emulator -avd test -no-audio -no-window &
        - android-wait-for-emulator
        - adb shell input keyevent 82 &
        - adb devices
        - adb install -r android/build/apk/android-debug.apk

    PS:这个集成方法的思路没有问题的,不过这个项目改来改去apk的生成估计已经有问题了,又或者Travis的环境还是有什么其他问题,所以会报错Invalid APK file,不过本地安装有时候又会成功。

  • 加密游戏数据与反内存编辑器

    内存编辑器如字面意义是可以修改内存的工具。可以很简单的使用于游戏破解。

    比如游戏中有金币这个概念,那么用户是可以看到金币数的,那么在内存编辑器中搜索这个数值,然后在游戏中做出一些操作使金币数量发生变化,然后再搜索新的值,直到搜索出来的结果数量只有一个或者几个,然后使用内存编辑器修改这个数值,回到游戏中金币数就会变成修改过后的值。 (更多…)

  • libGDX中的自动寻路

    Gdx-ai提供了常见的AI算法,当然也包括寻路算法,比如常用的A*。

    Gdx-ai自身的API抽象也很易懂,也方便自定义寻路。

    首先由Graph和Connection提供图中的连通性和对应连接的开销。 简单一点直接用DefaultConnection,它提供的默认开销为1。

    而PathFinder就是具体执行寻路的类,而返回的结果是GraphPath,它包含了路的结果,对应的连接还有其他一些数据。

    这是Meritxell Calvo Palanques画的示意图

    grafos

     

    另外就是Heuristic,它并不是必须的,但是一个有效的Heuristic函数可以加快搜索,不过不方便提供,或者不想提供,最坏的结果无非就是退化为Dijkstra。(我提供的例子会直接返回一个错误的启发函数,不过依然可以获得结果)。

    例子就简单一点吧,一般地图的数据是从Tiled地图或者其他格式的文件中获得的,这里我直接用硬编码来。

    这个图只有四个点,其中点1,2,3相互连接,点3连接点4,我们要寻找从点1到点4的路径。

    首先表示这个图

    IndexedGraph<Node> graph = new IndexedGraph<Node>() {
                @Override
                public Array<Connection<Node>> getConnections(Node fromNode) {
                    Array<Connection<Node>> connections = new Array<Connection<Node>>();
                    switch (fromNode.getId()) {
                        case 1:
                            connections.add(new DefaultConnection<Node>(n1, n2));
                            connections.add(new DefaultConnection<Node>(n1, n3));
                            break;
                        case 2:
                            connections.add(new DefaultConnection<Node>(n2, n1));
                            connections.add(new DefaultConnection<Node>(n2, n3));
                            break;
                        case 3:
                            connections.add(new DefaultConnection<Node>(n3, n4));
                            connections.add(new DefaultConnection<Node>(n3, n1));
                            break;
                        default:
                            connections.add(new DefaultConnection<Node>(n4, n3));
                    }
                    return connections;
                }
    
                @Override
                public int getIndex(Node node) {
                    return node.getId() - 1;
                }
    
                @Override
                public int getNodeCount() {
                    return 4;
                }
            };

    然后使用A*寻路并提供一个随机的启发函数

    IndexedAStarPathFinder<Node> pathFinder = new IndexedAStarPathFinder<Node>(graph);
            GraphPath<Connection<Node>> out = new DefaultGraphPath<Connection<Node>>();
            final RandomXS128 randomXS128 = new RandomXS128();
            pathFinder.searchConnectionPath(n1, n4, new Heuristic<Node>() {
                @Override
                public float estimate(Node node, Node endNode) {
                    return randomXS128.nextFloat();
                }
            }, out);

    最后从GraphPath<Connection<Node>> out获取数据即可

    a-star-path-finder

  • libGDX中的状态机实现

    LIbgdx旗下的gdx-ai库提供了大量使用的AI算法,其中包括了最常见的有限状态机FSM。

    gdx-ai是一个普通的java库,并不存在平台相关依赖,所以直接在core中引入就可。

    dependencies {
            compile "com.badlogicgames.gdx:gdx-ai:1.7.0"
        }

    StateMachine是有限状态机的接口,对应的实现目前有两个DefaultStateMachine和StackStateMachine。

    StackStateMachine以栈的形式保存状态,所以可以提供了方法获取上一个状态,一般使用DefaultStateMachine就行了。

    FSM的例子很多,我们直接来一个简单的。

    首先主体对象是人,而人只有两个状态,一个走路,一个打伞。

    状态切换的条件是时间,也就是假定下雨的时间符合一定的条件。

    首先创建一个Person类

    public class Person extends Image {
        private TextureRegion walk;
        private TextureRegion rain;
        private StateMachine<Person, PersonState> stateMachine;
        private float time;
    
        public Person(final TextureRegion walk, final TextureRegion rain) {
            super(walk);
            this.walk = walk;
            this.rain = rain;
            stateMachine = new DefaultStateMachine<Person, PersonState>(this, PersonState.WALK);
        }
    
        public void showRain() {
            setDrawable(new TextureRegionDrawable(rain));
            setSize(getPrefWidth(), getPrefHeight());
        }
    
        public void showWalk() {
            setDrawable(new TextureRegionDrawable(walk));
            setSize(getPrefWidth(), getPrefHeight());
        }
    
        public void update(float delta) {
            time += delta;
            stateMachine.update();
        }
    }
    

    当初始化时默认状态是没有下雨了,而界面的表现就是显示不同的图片

    walkrain

    而time就是总共经历的时间

    在最关键状态处理逻辑,包括进入,更新,退出,接受消息,是放在对应的State中的。这里我们不考虑接受消息的情况,所以永远返回false。

    public enum PersonState implements State<Person> {
        WALK {
            @Override
            public void enter(Person entity) {
                entity.showWalk();
            }
    
            @Override
            public void update(Person entity) {
                float v = entity.getTime() % 10;
                if (v >= 6 || v <= 4) {
                    entity.getStateMachine().changeState(PersonState.RUN);
                }
            }
    
            @Override
            public void exit(Person entity) {
            }
    
            @Override
            public boolean onMessage(Person entity, Telegram telegram) {
                return false;
            }
        },
        RUN {
            @Override
            public void enter(Person entity) {
                entity.showRain();
            }
    
            @Override
            public void update(Person entity) {
                float v = entity.getTime() % 10;
                if (v < 6 && v > 4) {
                    entity.getStateMachine().changeState(PersonState.WALK);
                }
            }
    
            @Override
            public void exit(Person entity) {
            }
    
            @Override
            public boolean onMessage(Person entity, Telegram telegram) {
                return false;
            }
        }
    }
    

    当在update中切换状态时,会自动触发对应的进入(enter)事件,所以也不需要额外的处理。

    切换状态时的对应代码实现

    @Override
    	public void changeState (S newState) {
    		// Keep a record of the previous state
    		previousState = currentState;
    
    		// Call the exit method of the existing state
    		if (currentState != null) currentState.exit(owner);
    
    		// Change state to the new state
    		currentState = newState;
    
    		// Call the entry method of the new state
    		if (currentState != null) currentState.enter(owner);
    	}

    参考资料:

    https://github.com/libgdx/gdx-ai/wiki/State-Machine

  • 使用AnnotationAssetManager管理Libgdx中的资源

    Libgdx对于资源管理提供了AssetManager来做资源的管理,如果你需要使用它那么你就不得不使用这样的代码

    manager.load("data/mytexture.png", Texture.class);
    manager.load("data/myfont.fnt", BitmapFont.class);
    manager.load("data/mymusic.ogg", Music.class);

    其中的文件路径是字符串。然后在你使用的地方

    Texture tex = manager.get("data/mytexture.png", Texture.class);
    BitmapFont font = manager.get("data/myfont.fnt", BitmapFont.class);

    这样使用其实蛮不方便的,如果你把文件路径作为一个全局变量的话,其实很多代码是重复的。

    而AnnotationAssetManager提供了反射的方法来完成这个工作

    package com.huangyunkun.hundred;
    
    import com.badlogic.gdx.graphics.Texture;
    import net.dermetfan.gdx.assets.AnnotationAssetManager;
    
    public class Assets {
        @AnnotationAssetManager.Asset(Texture.class)
        public static final String ball = "ball.png";
    }

    比如有一个球的图片,那么只需要声明一个字符串。然后加载的时候直接传入这个类本身,由加载器自己去寻找所有需要加载的资源即可。

    public class QuickStartScreen extends ScreenAdapter {
        private Stage stage;
        private AnnotationAssetManager annotationAssetManager;
    
        public QuickStartScreen() {
            this.annotationAssetManager = new AnnotationAssetManager();
            this.stage = new Stage(new StretchViewport(960, 480));
            this.annotationAssetManager.load(Assets.class);
            this.annotationAssetManager.finishLoading();
            this.stage.addActor(new Image(this.annotationAssetManager.get(Assets.ball, Texture.class)));
        }
    
        @Override
        public void render(float delta) {
            GdxUtilities.clearScreen();
            stage.draw();
        }
    }

    Asset类本身还可以根据需要拆成不同的小类。

    而@Asset注解还支持参数,比如

    @Asset(value = Texture.class, params = "textureParams")
    public static final String grass = "img/grass.png";

     

  • 构建可测试的libGDX应用

    构建可测试的libGDX应用

    Libgdx应用由于其特殊性,测试本身还是有一定难度的。好在Libgdx自身可以跨平台,在桌面平台调试/测试自然比在设备上方便不少。

    但是一个庞大的项目光看调试/测试是很难保证高可靠性的,特别是很多情况并不好测试,比如触发特定事件后的抽奖模块。

    如果可以使用一定数量的单元测试来保证应用的可靠性那自然是最好的了。

    分离绘制

    Libgdx应用测试的一个困难在于UI层面,基于opengl绘制出的界面并没有特定的自动化工具来完成测试,最简单的办法就是分离绘制和其他模块。

    你现有的Libgdx应用可能是这样的

    现有架构

    在render中放置了大量的代码,并在其中直接处理输入和其他逻辑,那么这种代码确实很难测试。

    如果你的应用是这个样子的

    实体系统

    那么大部分代码都可以直接被单元测试覆盖。

    Libgdx提供了ashley作为实体系统。

    利用Backend测试

    即使剥离了绘制,还是有大部分代码需要Libgdx环境。Libgdx环境是指除绘制以外其他模块,比如Gdx.files。

    这种情况下最简单的就是使用headless backend测试。

    这个backend有关gl部分是mock出来的,但是其他部分都是真实的,基本可以视为单元测试环境。

    在Web开发中我们喜欢提functional测试,即控制浏览器在页面上操作,并作出断言检测。

    其实利用Jglfw,我们依然可以做出类似的测试。

    我自己定义了一个Junit的Runner作为测试的辅助。这样的测试会打开一个界面,然后在测试完成后关闭。

    publicclass LibgdxRunner extends BlockJUnit4ClassRunner {
    private Random random =new Random();
    publicLibgdxRunner(Class<?> klass)throws InitializationError {
    super(klass);
    initApplication();
    }
    privatevoidinitApplication() {
    try {
    JglfwApplicationConfiguration cfg =new JglfwApplicationConfiguration();
    cfg.preferencesLocation = String.format("tmp/%d/.prefs/", random.nextLong());
    cfg.title ="Libgdx Runner";
    cfg.width =1;
    cfg.height =1;
    new JglfwApplication(new TestApplicationListener(), cfg);
    ConditionWaiter.wait(() -> Gdx.files !=null,"Jglfw init failed.",10);
    }catch (Exception ex) {
    thrownew LibgdxInitException(ex);
    }
    }
    @Override
    protectedvoidrunChild(FrameworkMethod method, RunNotifier notifier) {
    Description description = describeChild(method);
    if (description.getAnnotation(NeedGL.class) !=null) {
    final AtomicBoolean running =new AtomicBoolean(true);
    Gdx.app.postRunnable(() -> {
    if (isIgnored(method)) {
    notifier.fireTestIgnored(description);
    }else {
    runLeaf(methodBlock(method), description, notifier);
    }
    running.set(false);
    });
    ConditionWaiter.wait(() -> !running.get(), description,30);
    }else {
    runLeaf(methodBlock(method), description, notifier);
    }
    }
    privateclass TestApplicationListener extends ApplicationAdapter {}
    }

    这个Runner配合一个注解NeedGL使用,我在实际使用中,对于需要GL的测试加注解,对于不需要的,比如文件测试,还是使用的JglfwApplication

    来看一个测试

    @RunWith(LibgdxRunner.class)
    publicclass O2DDialogTest {
    private O2DDialog o2DDialog;
    @Before
    publicvoidsetUp()throws Exception {
    VisUI visUI =new VisUI();
    visUI.load(Gdx.files.local("assets/style/uiskin.json"));
    o2DDialog =new O2DDialog("O2DDialog Test");
    }
    @Test
    @NeedGL
    publicvoidshouldAddCloseButton()throws Exception {
    o2DDialog.addCloseButton();
    Table titleTable = o2DDialog.getTitleTable();
    assertThat(titleTable.getCells().size, is(2));
    assertThat(titleTable.getCells(), hasItems(
    hasProperty("actor", instanceOf(Label.class)),
    hasProperty("actor", instanceOf(VisImageButton.class))));
    }
    }

    这个O2DDialog会从visUI的Skin中创建默认样式的文字标签和按钮,而addCloseButton会添加一个关闭按钮,附带一个默认的Label。

    如果需要测试界面的响应,原理是一样的,但是绘制的内容需要在线程间共享,我这里用的是一个内置的消息队列来获取,点击和输入都可以直接调用Libgdx的代码来实现。

    如果需要在CI服务器(比如travis或者自己的jenkins),那么可能遇到一个问题就是没有GUI界面导致的x connection错误,这个时候使用xvfb即可。
    还可以使用xvfb来测试分辨的适配性。

    在travis中可以使用配置

    before_script:
    -"/sbin/start-stop-daemon--start--quiet--pidfile/tmp/custom_xvfb_99.pid--make-pidfile--background--exec/usr/bin/Xvfb--:99-ac-screen01280x1024x16"
  • libGDX游戏入门第二版

    libGDX游戏入门第二版

    Learning LibGDX Game Development是出版较早的关于Libgdx的书籍,前不久出了第二版。

    书是packt旗下的,定位是初学者级别,即对针对初次接触Libgdx的开发者。
    其中包含大量细致的步骤指导。

    第二版相较于第一版主要的变化在于:

    • Libgdx版本提升到1.x时代
    • 增加了3d相关内容
    • 增加了Gradle构建的相关章节

    当然,只有英文版的,在amazon能够买到,不过还是推荐买packt的kindle版,便宜又方便。

    地址:http://bit.ly/1zYQS1E

    如果上面地址访问不起,可以试试:https://www.packtpub.com/game-development/learning-libgdx-game-development-second-edition

  • 在游戏开发中应该使用测试驱动开发吗?

    测试驱动开发(TDD)逐渐成为越来越流行的实践,它提升了代码的易用性和独立性,也增强了开发信心。

    TDD作为一种实践自然不存在不适用的领域,但是在一些领域它是具有挑战性的,比如游戏开发。

    知你所愿

    测试驱动开发在游戏开发中应用的最大挑战并不是来自UI。
    其核心问题是你通常并不清楚你的UI是什么样的。UI是你必须面对的东西,而和它的交互更多时候是一个深层次的东西。

    游戏的本质是状态机,而获得某种状态或者到达某种状态有时候需要付出相当大的代价。

    测试这些东西是困难而且浪费的。

    Uncle Bob曾说过:‘不要TDD UI’。

    对于UI做纯粹的TDD是困难的,但是对于部分做TDD确实可能的。

    比如游戏中的算法,状态机本身的变化等等。

    对于UI它的确不合适,但是对于其他部分,TDD是一个不错的选择。

    单一责任原则

    遵循SRP原则是一个很好的补充。

    将你的代码分离出来,从某种意义来看这样是为了测试而改变,但是从长远来看,也是一种很好的维护方法。

    有很多事情是可以做到的,比如将可以测试的算法部分从UI中剥离,分离一些不同原因而存在的代码。

    有一点是需要牢记的,有些代码第一次看的时候很难测试,但并不意味着第二次的时候仍然很复杂。

    在流程上的妥协并不是什么大问题,首先使用你喜欢的方法写出代码,然后编写测试。

    你需要预防的只有一次性写完整个游戏再进行测试的冲动。

    在事后写测试的问题在于代码耦合,进而难以测试那些重要而有用的类。如果你发现你写的代码难以测试,请尽量遵循依赖倒置原则和开闭原则。

    这样才能保证代码解耦足够支撑你事后测试。

    实体系统

    将代码分离,说着很容易,操作起来确实困难的。

    如果分离?依照何种标准归类?

    Entity System本身就是一种分离规则,将整个游戏分离成组件、实体和系统。

    Libgdx有Ashley支持,它是一个轻量级的实体系统。

    一旦整个项目跟随这个规则来拆分,大部分代码都是可以测试的。

    游戏的核心部分将归入系统之中,而系统本身和绘制,即UI是可以分离的,这样游戏中的各种状态和情况都可以很快速的测试出来。

    BDD合适吗?

    BDD本身是一种设计方式,换一种方法思考本身并不能改变什么。

    从本质上讲,二者也不是一种东西。

    参考资料

    IEEE:TDD
    BDD
    Game engine 101

  • 升级Libgdx到最新版本

    升级Libgdx到最新版本

    Libgdx现在已经是1.X时代了,但是手上很多项目都还是在0.9.X上面,感觉越来越不顺手了。

    没法使用最新版本就意味着对于iOS的支持还停留在以前的解决方案中,即Xamarin+Monotouch。Xamarin收费不说,主要是慢而且要收费。

    看着别人快乐的使用最新版本,实在受不了了,开始升级之旅。

     

    基础Gradle配置

    最简单的办法应该是直接自己根据项目配置专属的build.gradle配置,然后将原有依赖移除,重新构建项目就行了。

    但是具体操作中感觉略麻烦,所以最后选择了新建初始化的项目,然后直接将旧的代码拷贝过去。

    建立初始化项目比较简单,直接使用setup工具就行了。

    我以DartsShaSha为例,这是一个简单的示例程序。

    setup

    创建好初始化项目以后直接拷贝原有的项目文件过来,然后运行./gradlew idea创建项目并引入Intellij IDEA。

    基础配置调整

    首先新版本已经移除了对于OpenGL ES 1.0的支持,原有配置的中的config.useGL10不再支持,默认启用OpenGL ES 2.0,config中可以不再书写。

    对于配置中的useGLSurfaceView20API18这一项,默认为false,就不要轻易改动了。

    Stage的缩放

    新增的Viewport类增强了Libgdx的屏幕自适应能力,原有的构造函数Stage(480,320,true)就不再适用,可以考虑使用new Stage(new StretchViewport(480, 320));替换。

    除了StretchViewport以外,可以选择的还有FitViewport、FillViewport、ScreenViewport、ExtendViewport和CustomViewport。

    一般情况用StretchViewport就够用了,不需要自己实现一个。

    一些细节

    有些细节有点复杂,比如Actor中的public void draw(SpriteBatch batch, float parentAlpha)变成了public void draw(Batch batch, float parentAlpha)

    我最开始没有注意到这个问题,绘制中很多东西都没有绘制出来,感觉特别奇怪。

    参考资料

    Viewport