Game with JDK [2] : Configuration

Frederic Delorme
5 min readNov 25, 2022

--

One of the first things you have to provide, before any game mechanic, it’s a way to set configuration values.

To satisfy this need, we will use a well-known java structure in a particular way: enum !

Introduction

First, we need to understand what would be the usage for our configuration. we need to define some value to be set by configuration (via a file) or fall back to a default value. the default model must support providing a description. We will also need to override the configuration value at execution time by providing a different value through the CLI.

  • name in the configuration file (a key configuration name),
  • a name for the command line interface ( a simple name),
  • a description,
  • a default value.

And as values come from the file or CLI, we need to get a converter from text to the real configuration attribute value we want to provide.

  • a converter to translate a String value to a Java Type (native or custom).

So our implementation will be a little bit more complex to allow an easy way to manage enumeration values:

A UML class diagram of the Enumeration and its interface

The attribute interface

the IConfigAttribute will be our interface to enumeration to define configuration attributes to be provided through a file of the command line:

public interface IConfigAttribute {
String getAttrName();
Function<String, Object> getAttrParser();
Object getDefaultValue();
String getAttrDescription();
String getConfigKey();
}

So we can first give an implementation of the enumeration :

public enum ConfigAttribute implements IConfigAttribute {
APP_TITLE(
"appTitle",
"app.main.title",
"Set the name of the application to be displayed in log and UI",
"GDemoApp",
v -> v),
DEBUG_MODE(
"debugMode",
"app.debug.mode",
"Setting the debug mode (0 to 5)",
0,
v -> Integer.valueOf(v));

//... other values will enhance the enum.

// internal attributes of the enum entry.
private final String attrName;
private final String attrDescription;
private final Object attrDefaultValue;
private final Function<String, Object> attrParser;
private String attrConfigKey;
ConfigAttribute(
String attrName,
String attrConfigKey,
String attrDescription,
Object attrDefaultValue,
Function<String, Object> parser) {
this.attrName = attrName;
this.attrConfigKey = attrConfigKey;
this.attrDescription = attrDescription;
this.attrDefaultValue = attrDefaultValue;
this.attrParser = parser;
}
// implementation of getters
//...
}

So referencing one configuration key will be nothing else than using the ConfigAttribut.DEBUG_MODE, for example.

But this is nothing without the Configuration system to retrieve and manage values.

Configuration class

The Configuration system will provide access to the configuration attribute values, from the file AND from the command line argument.

But first, initialize the attributes with their default values:

public class Configuration {
IConfigAttribute[] attributes;
private Map<IConfigAttribute, Object> configurationValues = new ConcurrentHashMap<>();
public Configuration(IConfigAttribute[] attributes) {
setAttributes(attributes);
// initialize all default values.
Arrays.stream(attributes).forEach(ca -> {
configurationValues.put(ca, ca.getDefaultValue());
});
}
//...
}

Now we can delegate the argument parsing to our Configuration class, to extract values from:

public int parseArgs(String[] args) {
boolean displayHelpMessage = false;
if (args.length > 0) {
for (String arg : args) {
String[] kv = arg.split("=");
if (!isArgumentFound(kv)) {
displayHelpMessage(kv[0], kv[1]);
return -1;
}
}
if (displayHelpMessage) {
displayHelpMessage();
}
}
return 0;
}
private boolean isArgumentFound(String[] kv) {
boolean found = false;
for (IConfigAttribute ca : attributes) {
if (ca.getAttrName().equals(kv[0]) || ca.getConfigKey().equals(kv[0])) {
configurationValues.put(ca, ca.getAttrParser().apply(kv[1]));
found = true;
}
}
return found;
}

We also can display a help message in case of an error during argument parsing:

private void displayHelpMessage(String unknownAttributeName, String attributeValue) {
System.out.printf("INFO | Configuration : The argument %s=%s is unknown%n", unknownAttributeName, attributeValue);
displayHelpMessage();
}
private void displayHelpMessage() {
System.out.printf("INFO | Configuration : Here is the list of possible arguments:%n--%n");
Arrays.stream(attributes).forEach(ca -> {
System.out.printf("INFO | Configuration : - %s : %s (default value is %s)%n",
ca.getAttrName(),
ca.getAttrDescription(),
ca.getDefaultValue().toString());
});
}

And when at least we get some configuration values, we can get it :

public Object get(IConfigAttribute ca) {
return configurationValues.get(ca);
}

And, as we discussed before, we can retrieve configuration values from a file, we will use a properties file (a standard java way to set values) :

public Configuration setConfigurationFile(String cfgFile) {
Properties props = new Properties();
try {
props.load(Configuration.class.getResourceAsStream(cfgFile));
for (Map.Entry<Object, Object> prop : props.entrySet()) {
String[] kv = new String[]{(String) prop.getKey(), (String) prop.getValue()};
if (!isArgumentFound(kv)) {
System.err.printf("ERR | Configuration file=%s : Unknown property %s with value %s%n",
cfgFile,
prop.getKey(),
prop.getValue());
} else {
System.out.printf("INFO | Configuration file=%s : set '%s' to %s%n",
cfgFile,
prop.getKey(),
prop.getValue());
}
}
} catch (IOException e) {
System.err.printf("ERR | Configuration file=%s : Unable to find and parse the configuration file : %s%n",
cfgFile,
e.getMessage());
}
return this;
}

You can notice that errors are output on the System.err output stream and information are out on the System.out output stream.

Note: you may noticed I haven’t use a logger library (even not the JUL). Yes, I know, the goal is not to do the best implementation, but the most useful and the simplest one. and we create a game, not a web application on a server ;)

Executing the “gradle run” will get the following output:

❯ gradle run
Starting a Gradle Daemon, 3 incompatible Daemons could not be reused, use --status for details

> Task :app:run
INFO | App : Start GDemoApp
INFO | App : - initializing...
INFO | Configuration file=/config.properties : set 'app.test.counter' to 2
INFO | Configuration file=/config.properties : set 'app.main.title' to GDemoApp
INFO | Configuration file=/config.properties : set 'app.render.fps' to 60
INFO | Configuration file=/config.properties : set 'app.debug.mode' to 1
INFO | App : - initialization done.
INFO | App : - create stuff for GDemoApp
INFO | App : - Loop 0:
INFO | App : - handle input
INFO | App : - update thing 0,000000
INFO | App : - render thing at 60 FPS
INFO | App : - Loop 1:
INFO | App : - handle input
INFO | App : - update thing 20,000000
INFO | App : - render thing at 60 FPS
INFO | App : debugMode=1: Main game loop executed 2 times (as required 2).
INFO | App : End of GDemoApp

BUILD SUCCESSFUL in 20s
3 actionable tasks: 3 executed

Conclusion

We spent maybe too much time on configuration, but you may see that this is essential.

Like in the previous episode, you will find the corresponding code (and a lot more!) on this GitHub repository https://github.com/SnapGames/game101 on tag create-config and create-config-test proposing some unit tests on this topic.

That’s all folks!

McG.

--

--

Frederic Delorme

Platform Architect at BigPharma during days and craftman developer during nights