ManaWars CastleDefense is a cross-platform game written mainly in Kotlin. I made the game design and coded the software and my amazing brother Moritz created the artwork (e.g. characters, backgrounds, UI elements). It follows the previous ManaWars sidescroller game we have created. I, personally, have put quite a lot of time into the software architecture of the game and made sure it is flexible and easy to update and extend. The source code is available on Github (Link).
Short demo video about the current state:
The game looks quite nice (I think) and most parts of the code are already implemented. The only issue is: The game currently is not as much fun as I had anticipated. An in-depth discussion about the fun of the game and game design, in general, can be found in the GameDev Forum (Link).
Some code details
The game code consists of three main packages:
- „game“ contains very basic and generic game classes and components that handle basic game entities, the camera, input, screens and other very low-level things
- „manawars“ contains ManaWars-specific code, such as ManaWars characters with their unique animations based on skin files, their skills, upgrades, and general menu page support. It depends on „game“
- „castledefense“ contains high level castle defense game mode code and depends on „manawars“. It contains the actual game menus, handles enemy tribes, armies, and castles and is responsible for the player profile and player data
Writing this text, I see that a few adjustments could be made, such as moving file storage handling down one level: from „manawars“ to the „game“ level. Despite those minor things, the code still strictly decouples the code in such a way, that low-level packages do never depend on high-level code, and because of dependency inversion (e.g. just adding suitable interfaces as a layer of abstraction between high and low-level code) the high-level code does also not depend on implementation details of low-level code. As you can see, for example, in the manawars/handlers package, most function implementations are hidden behind solid interfaces, which will barely ever change.
Game entities (e.g. characters, skills, buildings) use the classes contained in the „entities“ packages. Because the main structure of entities really is fixed, inheritance is used here (I know some people completely hate inheritance because of its tight coupling). GameEntity inherits from GameRectangle, GameEntityMovable inherits from GameEntity, and so on and so forth. The parts that CAN indeed differ from entity to entity are 1. look and 2. behavior. Therefore, those are not contained within the entity classes themselves but provided by external classes, using composition. MEntityAnimated, for example, supports any kind of animation meeting the expectations of the given interface. In praxis, this means anyone could add new animation types for characters, by just implementing the IEntityAnimationProducer interface and the IEntityAnimation interface. So far, the animations we have implemented are human animations, building animations, mount (e.g. horse or wolf) animations, pet (e.g. flying dragon) animations, and rider (e.g. mount plus human) animations. In a similar fashion, the controllers of entities can be replaced at any time: entities support any controller that implements the interface IController.
While in the past I just hard-coded actual game content (such as characters/maps/skills) in Java, I have come to the conclusion that it makes sense to provide this kind of content in an independent format, such as YAML files. This way, the content can be re-used even if the whole game should be rewritten in another language at any point. Another advantage is the fact, that the content in those YAML files is actually easier to read, understand, and maintain.
One example of game content provided by YAML files is actions/skills. While the abstract DataSkill class is generic and accepts skill data from anywhere, the concrete class DataSkillLoaded reads the expected data from a provided configuration file. This configuration file could be a JSON or a Yaml file (see: another level of abstraction right there :D). In case of skills, those configuration files are located in the android/assets/content folder.
Almost all of the actual game content data is provided by such YAML files.
Entities of instance MEntityActionUser can, as the name suggests, use/perform actions. What those actions are is not defined by the MEntityActionUser class though. Instead, every class that implements the IDataAction interface represents a valid action which the entities can use. The interface requires classes to implement a function performing the actual action (#action(owner)), a function that checks whether the given entity is able to use the action at the given time (#canUse(owner)) and the range of the action (e.g. in order to use an axe attack, the player needs to be close to enemies, using a range of 300 to 500 pixels). Other than that, a few cosmetic details are required, such as the name, a preview icon, a sound (which is optional though) and a few more things.
Now, the actions we currently have in the game are either single skills (e.g. one fireball), a combination of multiple skills (e.g. hail of arrows, consisting of multiple arrow skills), or the healing action, but the architecture keeps the option of adding other kinds of actions open.
As mentioned in the „YAML Files“ section, instead of hardcoding the skills via code, I use YAML config files, from which those skills are read and them loaded.
Because during the development and testing of a piece of software, you always get new insights and ideas about how the software can be improved, I think it is absolutely vital to ensure the software remains soft, meaning it remains easy to extend and easy to modify at any time. Therefore, the amount of decisions already taken is minimized: while over 90% of the code of the game is already implemented, one could still alter the gameplay in hundreds of different ways, and the code would support it and be ready for it. Actually, in multiple cases, I faced two different viable options and just implemented both of them, leaving the decision for which one to choose open for later. For example, in the MConstants class, there are multiple such global settings defined, such as the attribute SKILLS_COLLIDE_WITH_EACH_OTHER , which can enable/disable a mechanic that makes skills, well, collide with each other, when they hit each other, causing the stronger skill to destroy the weaker skill. The class CDConstants serves the same purpose, this time just for the castledefense package, instead of the manawars package. One might argue that keeping different options open could increase complexity, but I would, actually, say it helps to create a good architecture because it favors reduced coupling and increased modularity of components
A topic one could criticize is the lack of tests (there are less than 5 unit tests). I do agree with that criticism, although I must say, that in the case of this game, I am the only software developer working on it and I do still understand what my classes and functions are supposed to do (I really do) and the game works really well. Also, the development process was really smooth, fast, and enjoyable. Because there is always visual feedback, it never took a lot of time to find the cause of a bug. If any other developer should join this project, it would, for sure, be necessary to add comprehensive unit tests, to make sure one can develop quickly without being afraid of breaking something. Luckily, the lack of tests was not a problem for me so far, but in the case of working together with other developers, tests, of course, make sense and are an important pillar to achieve high code quality.