LibGDX开发日常#1

本文非面向libGDX入门用户,仅为个人日常记录。

相关关键词:gwt, gdx-lml, dependency injection, eventbus

万恶的GWT

说GWT是个好东西吧,这玩意毕竟做了个Java编译Javascript的兼容,但是实际用起来的约束非常多,一个是反射,一个是多线程,受到这两个约束,很多Java的库反而都不能用了。

然后GWT调试也很恶心,甚至打包也经常有资源更新不上的情况,superDev还整天端口占用,更别说有些什么整型溢出之类的奇葩问题,desktop跑得好好的尼玛的打成GWT就尼玛的爆炸了,我真是操你妈的!(还是自己的平台好,想爆粗就爆粗)

谁叫我想打包web平台呢,teavm现在又不成熟,只能吃了这口屎。

gdx-lml是个好东西,虽然有点小瑕疵

之前还并不是很想看这东西,一来人家项目都archive了,二来这种玩意他是第三方开发的,就像之前用skin-composer的时候,一个很明显bug的情况(https://github.com/raeleus/skin-composer/issues/118), 居然自功能开发以来就没人发现过。

但是不用第三方库自己写UI是不可能的,用代码创建UI基本上不可能些什么复杂界面了,这也是为什么折腾了gdx-lml-vis。这东西看了才发现,好家伙,作者以前肯定是个写安卓的,一股ButterKnife的既视感。

不过这玩意的设计也有点问题啊,LmlParser创建View的方式里居然没有可以传入Stage的方式,明明他的设计里面,AbstractLmlView里的默认构造方法是AbstractLmlView(Stage),却默认派生的类都使用拥有默认构造函数,虽然他确实留了个填入view对象的createView方法留了条活路就是。

Basically, LmlApplicationListener is the same thing to AbstractLmlView that LibGDX Game is to Screen – only it’s packed with more features.
[https://github.com/czyzby/gdx-lml/wiki/Your-first-LML-application#long-live-the-king]

把LmlView当Screen这个设计真的不敢苟同。

这设计思路就像认为所有Screen都只有一个Stage,但实际上一个Screen可能含有多个Stage。而且为了实现这种单Stage的绑定关系,他是拿AbstractLmlView去管理Stage了,而实际上LmlView才是应该是被Stage管理的对象.反正我不认同用这种设计的,我的core工程主类也不会继承LmlApplicationListener。甚至,我不会去使用AbstractLmlView,我只会通过在自定义Stage中实现LmlView和ActionContainer,从而实现Stage和LmlView的绑定,以及保留对多Stage的支持.

这样做就没法在LML使用预先定义在LmlPllicationListener中的方法,但我恰恰就是不想要这些预定义接口,如exitsetView等的。这些接口本就是自己加一个GlobalAction的事,非要整成预定义的接口,一个不小心还可能跟预定义的接口重定义了。

简单依赖注入

习惯了Spring,还是会比较想要用依赖注入的。Lml能解决UI的注入,但是一些非UI组件的注入还是要另想方法。

但多数通用的依赖注入框架都比较麻烦,不管是Dagger还是Guice等,基本上都为了自由地对依赖进行更换,对构造工厂增加有各种module的拆分。module拆开在做单元测试的时候挺好的,只需要对依赖做最小的替换,但是我这做小软件小游戏啥的没必要整那么深的层级关系,所以还是自己写了一套简单的注入框架,只需要能指定某个类型所使用的对象就好了。

大概实现如下:

public class Injections {
    private Map<Class<?>, InjectionSupplier<?>> suppliers = null;
    public void registerServices(BatchRegister batchRegister) {
        Map<Class<?>, InjectionSupplier<?>> tempSuppliers = new HashMap<>();
        batchRegister.batch(new ServiceRegistrar() {
            @Override
            public <T> void registerPrototype(Class<T> clazz, Supplier<T> supplier) { tempSuppliers.put(clazz, new InjectionSupplier<>(false, supplier)); }
            @Override
            public <T> void registerSingleton(Class<T> clazz, Supplier<T> supplier) { tempSuppliers.put(clazz, new InjectionSupplier<T>(true, supplier)); }
        });
        suppliers = Collections.unmodifiableMap(tempSuppliers);
    }

    public <T> T get(Class<T> clazz) {
        return (T) suppliers.get(clazz).get();
    }

    public interface ServiceRegistrar {
        <T> void registerPrototype(Class<T> clazz, Supplier<T> supplier);
        <T> void registerSingleton(Class<T> clazz, Supplier<T> supplier);
    }

    @FunctionalInterface
    public interface BatchRegister {
        void batch(ServiceRegistrar registrar);
    }

    @Getter
    public static class InjectionSupplier<T> {

        private final boolean singleton;
        private final Supplier<T> supplier;
        private volatile T singletonInstance;

        public InjectionSupplier(boolean singleton, Supplier<T> supplier) {
            this.singleton = singleton;
            this.supplier = supplier;
        }

        public T get() {
            if (!singleton) {
                return supplier.get();
            } else {
                if (singletonInstance == null) {
                    synchronized (this) {
                        if (singletonInstance == null) {
                            singletonInstance = supplier.get();
                        }
                    }
                }
                return singletonInstance;
            }
        }
    }
}

为了节省篇幅能省的都省了,注入对象的使用也非常简单,只要在应用开始的时候给对应的类型注册对象的构造方法就行了:

injections.registerServices(registrar -> {
            // Singleton injections
        registrar.registerSingleton(A.class, AImpl::new);
            // Prototype injections
        registrar.registerPrototype(B.class, BImpl::new); 
});

A a = injections.get(A,class);
B b = injections.get(B.class);;

反正就是突出一个简洁。

消息发布订阅

既然都解耦成这样了,于是就把发布订阅也写了吧。其实我还是很想用EventBus的,不管是greenbot的还是guava的,但是消息订阅发布的库基本上都有多线程和反射,rxJava同理,鉴于我要用gwt基本上还是别想了,自己手写一个简单的用于解耦就好了。

由于不能用反射,所以事件的类型只能老老实实在注册和发布的时候把类型传进去了。
但是接受消息的我想不到使用反射以外的方法了,毕竟我总不能实现一个EventListener之类的接口吧,如果只有继承回调接口,那接收的消息类型一旦多了就很恶心了,回掉接口得有一堆条件判断,那我还是用回反射吧,反正注册回调的多数都是ScreenStageViewActor,这些类型的代码基本上都放在ui包的,全部一起喂给GWT预处理反射信息就好了。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscribe {
}

// ===============================

public class EventBus {

    /** 什么,你问我MultiMap从哪里来的?我当然不会告诉你Guava有相当部分是GwtCompatable的 */
    private final Multimap<Class<?>, Pair<Method, ?>> subscribers;

    public EventBus() {
        subscribers = ArrayListMultimap.create();
    }

    @Override
    public <S> void registerSubscriber(Class<S> subscriberClass, S subscriber) {
        for (Method method : ClassReflection.getDeclaredMethods(subscriberClass)) {
            if (method.isAnnotationPresent(Subscribe.class) &&
                method.getParameterTypes().length == 1) {
                subscribers.put(method.getParameterTypes()[0], Pair.of(method, subscriber));
            }
        }
    }

    @Override
    public <S> void unregisterSubscriber(Class<S> subscriberClass, S subscriber) {
        for (Method method : ClassReflection.getDeclaredMethods(subscriberClass)) {
            if (method.isAnnotationPresent(Subscribe.class) &&
                method.getParameterTypes().length == 1) {
                subscribers.remove(method.getParameterTypes()[0], Pair.of(method, subscriber));
            }
        }
    }

    @Override
    public <E> void post(Class<E> eventType, E event) {
        subscribers.get(eventType)
            .forEach(pair -> {
                try {
                    pair.getFirst().invoke(pair.getSecond(), event);
                } catch (ReflectionException e) {
                    throw new RuntimeException(e);
                }
            });
    }

}

于是一个简单的EventBus就完成了,使用起来大概这样子:

public MyScreen implements Screen {
    public MyScreen() {
        eventBus.registerSubscriber(MyScreen.class, this);
    }

    @Override
    public void dispose() {
        eventBus.unregisterSubscriber(MyScreen.class, this);
    }

    @Subscribe
    public void onEvent(Event1 event) {
        Logging.debug("MenuView.onEvent", "event = {}", event);
    }
}

eventBus.post(Event1.class, new Event1())

不过因为注册和反注册都使用了泛型函数约束了两个参数的类型,如果要在基类实现统一注册和反注册需要使用奇异递归模板传入子类的真实类型。像这样:

public abstract class BaseScreen<T extends BaseScreen> implements Screen {
    
    public BaseScreen() {
        //noinspection unchecked
        Class<T> concreteClass = (Class<T>) getClass();
        //noinspection unchecked
        eventBus.registerSubscriber(concreteClass, (T) this);
    }

}

整体整下来,这下Gdx的UI控件开发体验有点像安卓了,剩下的就是要花时间熟悉LML里每个控件的属性和配置了。有种要学CSS的大难临头感 Orz.

Leave a Reply

Your email address will not be published. Required fields are marked *