Large parts of GDX-Liftoff were rewritten in Java in order to use less "behind-the-scenes magic", which seemed impossible to avoid in the existing Autumn+LML-based Kotlin codebase. Avoiding LML allowed more advanced scene2d.ui techniques to be used without hacking up the LML codebase or attempting to make sense of its limited documentation. Parts of this app still use Kotlin, but for the data handling and sample generation primarily, with no code currently using Autumn, Autumn MVC, or LML. Kotlin's actually quite nice for these purposes, and if gdx-liftoff were rewritten in Kotlin today, more tools could be used that weren't available when this project started (like the many KTX libraries). A big part of why the Java rewrite made sense was simply familiarity with Java and a lack of it with Kotlin -- learning how to write basic Kotlin is easy, but learning how to write high-quality, maintainable Kotlin is as hard as it is in any other language, and many of the skills are simply not easily transferred from developing Java to Kotlin.
Also, if you happen to be writing a library for wide usage, and you think a custom language is a great idea, it isn't. If you have millions of dollars to spend on getting documentation authors to document your language, it still isn't. This world has many existing formats, some of which are very extensible, that don't need a Herculean effort to document their various pitfalls and snares to catch the unwary.
Gdx-Liftoff already contains quite a few languages by necessity: Kotlin, Java, Gradle (Groovy), "properties," XML, JSON, shell, batch script, AngelCode BMFont, libGDX .atlas (both new and old formats), MarkDown, ProGuard/R8 configuration, and probably more. Oh yeah, .editorconfig uses TOML.
We don't need more. We don't want more.
gdx-liftoff is a copy of czyzby/gdx-setup application, sharing the code
general structure and technology stack. The main frameworks used throughout the application include:
- libGDX.
- LML: GUI builder.
scene2d.uitemplates with HTML-like syntax. - Autumn: dependency injection framework.
- Autumn MVC: model-view-controller framework based on Autumn and LML.
- VisUI: flat design
scene2d.uiskin with numerous additional widgets.
By today's standards, gdx-liftoff is not a typical Kotlin libGDX application. The tool was started when Kotlin
was a relatively new language, and Kotlin extensions for libGDX such as KTX did not
exist. The project was a joint effort between @czyzby (the author of the gdx-lml
libraries) and @kotcrab (the author of the VisUI framework).
This application was also meant as an example use case of the Autumn MVC framework, which admittedly was not the best tool for the job. Autumn MVC does have some advantages, such as UI hot reload or multi-platform dependency injection with automatic scanning that significantly cuts down on glue code and boilerplate. However, it is a heavily opinionated framework that is rather complex, and does quite a lot under the hood. Additionally, the framework itself was implemented with Java in mind, making many APIs feel clunky in Kotlin.
Using a non-standard technology stack, gdx-liftoff can be difficult to extend for new contributors. This document
aims to make it a bit easier by explaining how the application actually works.
- main.kt is the main entrypoint of the application. It tries to run a libGDX application, which some additional code to avoid platform-specific issues.
AutumnApplicationis the implementation of the libGDXApplicationListener. It handles the lifecycle and rendering of the application, delegating each application event to an internal Autumn MVC service.DesktopClassScanneris used to scan the classpath in search of classes annotated with Autumn component annotations.- Application components can be annotated with annotations such as
@Component,@Property,@View,@ViewDialog, or@ViewActionContainer. - The list of standard Autumn and Autumn MVC annotations is extended with custom application-specific annotations such
as
@Extension,@ProjectTemplate,@JvmLanguage, and@GdxPlatform.
- Application components can be annotated with annotations such as
- All the annotated classes are found in the classpath during the application startup, constructed with reflection,
filled using dependency injection, and registered in the Autumn context as singletons.
- Autumn injects values into fields annotated with
@Inject. - Autumn MVC processors handles some fields annotated with annotations such as
@Preferenceor@StageViewport. These are standard Autumn MVC features that are mostly used to configure the Autumn MVC application settings in Configuration.kt. - Autumn MVC handles application preferences from the
preferencespackage. They include some values such as package name of the generated project which are cached from the previous tool usage. - Custom annotation processors ensure that this step also creates classes representing official and third-party extensions, project templates, languages, and libGDX platforms. A respective annotation is sufficient to register them in the application, and they require no further setup.
- Autumn injects values into fields annotated with
- Component methods annotated with
@Initiateare called after the components are constructed.- Configuration.initiate loads a custom VisUI skin and extends the LML parser with custom tags representing additional GUI widgets.
- Application views and dialogs are constructed from LML templates.
LmlParserconfigured in Configuration.kt handles LML templates and associated controllers.- LML templates are stored in
resources/templates/.- Tags within LML templates represent individual Scene2D widgets displayed in the views.
- Tags starting with
:are macros that implement basic scripting functionalities such as iteration or conditionals.
- Classes annotated with
@ViewActionContainersuch as GlobalActionContainer define functions available in the LML templates.@LmlActionannotation specifies IDs of the functions as they appear in the templates. - Classes annotated with
@Viewand@ViewDialogrepresent view controllers. They point to their corresponding LML templates. Their fields annotated with@LmlActorand@LmlInjectare constructed and injected by theLmlParserafter their template is parsed.- View methods annotated with
@LmlActioncan be executed from within LML templates. Notably, their results can also be handled by the parser to display Scene2D widgets. That is how data such as available libraries, platforms, languages, or project templates are exposed to and displayed by the LML templates. - Types of fields annotated with
@LmlInjectcan have nested LML annotations such as@LmlActor. They will be handled recursively by theLmlParser. - If a value of
@LmlInject-annotated field isnull, a new instance will be created using reflection. If it already exists, it's annotated field will be handled by the parser, but the instance will not be replaced. That is why some fields can be annotated with both@LmlInjectand@Inject- in such cases Autumn will handle dependency injection andLmlParserwill process nested fields. - LML annotations are strictly related to the UI and mostly handle injection of Scene2D actor instances.
- View methods annotated with
- MainView is set as the initial (and the only) application view.
- This view will be continuously rendered by Autumn MVC
InterfaceService. - Actors defined by the main.lml template are displayed on the application's
Scene2D
Stageand handle user input.
- This view will be continuously rendered by Autumn MVC
The results of the initiation are as follows:
- All annotated application components are created via reflection and filled by a dependency injection framework.
- All components such as libGDX libraries, platforms, languages, or project templates are created and registered.
- MainView is constructed from a LML template and rendered.
- Clicking on the non-disabled "Generate project!" button shows the GenerationPrompt dialog.
- Before the dialog is shown, it launches a task on another thread that handles actual project generation.
- A Project is created with the current application data.
- Project data is based on the state of the GUI. The selected libraries, platforms, languages, and template are passed to the project.
Project.generategenerates the project files.addBasicFileshandles generic files such as.gitignore.addJvmLanguagesSupportconfigures Java and additional selected JVM languages based onLanguageclasses.addExtensionshandles official and third-party extensions defined byLibraryclasses, setting Gradle properties with library versions, and adding appropriate Gradle dependencies to defined modules.- This step also fetches the latest extension versions from the corresponding repository such as Maven Central or JitPack.
template.applygenerates project sources according to the chosenTemplate.addPlatformsadds platform-specific files of each selected libGDXPlatform.addSkinAssetsoptionally adds Scene2D skin assets to the project.addReadmeFiledefines a README file based on the selected project configuration.savePropertiesgenerates agradle.propertiesfile.saveFilessaves the defined project files in the selected project location.
Project.includeGradleWrapperadds the Gradle wrapper files.- The prompt dialog buttons are no longer disabled after the project is generated. The dialog can be closed.
Add a @JvmLanguage annotated class implementing the Language interface to the
languages package. Add the necessary dependencies and files to the
project in the initiate function.
Add an @Extension(official = true) annotated class extending the OfficialExtension class to the
officialExtensions.kt file. Add library
dependencies in the initiate method. Add a Gradle property with the library version if the extension does not share
the libGDX version.
The id property defines the unique ID of the library throughout the project. To ensure that the library is properly
described in the tool, add id= and idTip= entries to the nls.properties
file (replacing the id with a custom one) with the formatted library name and short description. For example:
myLibrary=My Library
myLibraryTip=Generic libGDX utilities.Add an @Extension annotated class extending the ThirdPartyExtension class to the
thirdPartyExtensions.kt file.
Add library dependencies in the initiateDependencies method. Note that the library version based on the chosen id
will already be added to gradle.properties, and the addDependency method does not require passing the version.
If you need to define a dependency with other version than the library itself, use the addExternalDependency method.
The id property defines the unique ID of the library throughout the project. To ensure that the library is properly
described in the tool, add id= and idTip= entries to the nls.properties
file (replacing the id with yours) with the formatted library name and short description.
Add a @GdxPlatform annotated class implementing the Platform interface along with a class representing the Gradle
file that extends GradleFile. Add necessary files in the initiate method. If any platform-specific libraries have
to be added to support the backend, modify the necessary extension classes.
See the current backends in the platforms package.
Add a @ProjectTemplate annotated class implementing the Template interface to the
templates package. If the template uses only official libGDX APIs,
use @ProjectTemplate(official = true) annotation and put the class in the official package. Otherwise, the template
file should be in the unofficial package. Implement the Template interface methods generating all the necessary
files. Include any dependencies that are required to run the template. See existing project templates for details.
All GUI changes have to be made in the LML templates and the corresponding view controllers.
See the LML wiki for more info about the framework.
Translate the nls.properties file. Create an issue or submit a pull request with the translation. While the application is capable of supporting different locales, currently only English translation exists.