Of course, this greatly increased the volume of work. I'm not a designer or artist, I've only ever done gameplay programming professionally and a small amount of engine level programming while porting a game from a custom engine to UE4. I'm definitely still learning a lot of these skills as I go on, but I'm relatively happy with how the art and design have been going so far. A huge reason for doing this project from scratch is to gain more engine level experience since I'm much more interested in more complex engine work than gameplay programming.
In these blog posts I aim to detail different aspects of Nirion's development. I think these will be mostly technical and will generally just describe how I've chosen to do things. If you start to have any questions or want to see a particular topic covered, I'd be happy to go over that. This blog post will just be a light introduction to different aspects of the game so you can get familiar with what the game is and how it's been built so far.
Story and Setting
I won't go into the story in too much detail right now, but I'm aiming for a simple story that mostly doesn't get in the way of the gameplay. The basic premise is that the player takes control of a "house robot". These robots are basically just simple robot that are designed to do chores around the house, but are also built to be extended with different(usually just house utilities) upgrades. A family will own one of these robots like a pet in this world and are generally treated as part of the family. During some events at the start of the game, the robot and his family wind up stranded separately on a mountain named "Nirion", and this is where the game takes place. At this point the story takes a back seat and more details will be given through optional cutscenes until the player gets closer to the ending.
Concept art of the Melee Wasp enemy by my partner. I convert this into pixel art.
The Nirion mountain is broken up into several areas which the player explores. There are many (mostly bug inspired) creatures who inhabit each area of the mountain, as well as environmental dangers such as lava and plasma. Although most of the world is organic, there are hints of technology around, usually deeper inside the mountain. The player begins in the "Mountain Side" area and is quickly directed into the Mines.
Concept art of the Spike Crawler enemy by my partner. I convert this into pixel art.
General Gameplay
As mentioned above, the gameplay is heavily inspired by Super Metroid and A Link to the Past. The game world is one huge interconnected area with zero screen transitions. The player should generally only be blocked by required upgrades and not by story progression or arbitrary boundaries. This kind of world design is mostly inspired by Super Metroid, but the perspective is obviously closer to A Link to The Past.
Moving and combat are done with a "twin stick shooter" approach, where the player controls the robot's movements with the left stick and aims weapons/items with the right stick. The shoulder buttons are used to activate items and other abilities. Throughout the game, the player will pick up upgrades which will change different aspects of the controls and also add to combat and exploration options. Controlling the player character in exploration and combat should feel fundamentally different at the start of the game versus the end of the game.
Combat is focused on three different player abilities which are obtained fairly close to the start of the game. These are:
- Attacking. This can be melee and ranged.
- Dodging. This allows the player to quickly and safely move out of the way of enemy attacks. It's easier to do, but is generally less rewarding.
- Blocking. This allows the player to block melee attacks, potentially stunning the enemy, or reflect projectiles back at the enemy. It's riskier, but is more rewarding when executed correctly.
The player dodging an attack
The player blocking an attack. This weak enemy is destroyed immediately as a result.
These three concepts are the focus of combat. Most normal enemies found throughout the game are more like obstacles than difficult combat challenges, but boss fights will use this extensively. Some player combat attributes like max health and attack/defense are increased by finding optional upgrades. The more the player explores and finds secrets, the easier combat will get.
The player attacking an enemy
Dependencies
The game is written from "scratch" using C++. The game only supports windows right now and uses only OpenGL to render. I've tried to separate platform and rendering APIs in a reasonable way, very similar to how it's done on Handmade Hero. The game includes an asset preprocessor which is used to process all the files in the "data" directory and output them as a more optimised format in a single file. In this process, I currently rely on stb_TrueType.h to render bitmap fonts, but I do eventually plan on doing this myself one day. The game also relies on a few CRT functions such as sin, memcpy and qsort.
High Level Structures and Editor
Most of the gameplay code is related to the Entity data structure. All things in the world are an entity in some way. The player is an entity, all the items the player can use is an entity, and tilemaps are an entity(although each individual tile isn't). Using code generation, the entity structure can be extended and new behaviors can be added.
A typical entity definition in code would look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ENTITY_DATA ( struct animatorEntity { u32 animation; i32 loopCount; EntityId attach; }; ); ENTITY_DEFAULT_FUNCTION(EntityDefault_Animator) { return CreateEntityDefault(entity, true); } DEFINE_ENTITY(hash="Animator", type=ET_Animator, func_Default=EntityDefault_Animator, func_Constructor=EntityConstructor_Animator, func_Update=EntityUpdate_Animator); |
This piece of code defines extra data the entity needs, how it should be initialized by default, and some information about the entity such as it's type and what functions to call for common behaviors. Default functions are called only once at start up to create a default version of each type of entity. Any entity instance loaded from map assets is saved as a per property difference from this default entity. What I mean by that exactly is that when an entity is saved to the map asset in the editor, only it's properties that are different from this default are saved. When an entity is spawned, you can pass in a data structure containing all changed properties to spawn a specific instance.
Entity functions are also defined here, allowing you to implement different logic for different entities. The code generator will look at this and generate a switch statement for each type of entity function, switching on the entity type and calling the given function. Personally I feel like separating entity code based on types is usually easier for me to work with. I found having large, manual switch statements for this was very difficult to work with as the code grew larger.
Entities also define zero or more "EntityVisual" and "Geometry" instances. EntityVisuals just allow entites to define a visual component. This can be a static bitmap, an animation, a particle system, or even an entire tilemap. Geometry instances are components for use by the physics engine. An entity can be made up of multiple of these, but the physics engine will just treat them all as one rigid body. Geometry is ultimately used by the GJK algorithm, so they can be arbitrary polygons and are defined as an asset in a text file.
1 2 3 4 5 6 7 8 9 10 11 12 | ENTITY_DEFAULT_FUNCTION(EntityDefault_UpgradePickup) { EntityVisual* container = PushEntityVisual(system, entity, BID_Common, BID_NormalMapFlat, Rect2(V2(0.0f), V2(16.0f) * ReferenceMetersPerPixel), Rect2(0.0f, 240.0f, 16.0f, 16.0f), 0.0f, RenderSort_Base); container->params.bitmap.isSubPixel = true; entity->physicsEntity.isInUse = true; entity->physicsEntity.type = PT_Entity; PushPhysicsGeometry(&entity->physicsEntity, GID_Square, CreateTransform({}, 0.0f, V2(0.8f)), &pickupResponseTable); ENTITY_EDITOR_NAME("Upgrade Pickup"); return CreateEntityDefault(entity, true); } |
The upgrade pickup entity defined in the code above. It can be picked up by the player thanks to it's physics geometry.
Entities can only be defined in code. Considering I'm the only one working on this project, I just don't think defining entities anywhere else is that useful. In general the editor should not be used for making large or complex differences in entities from their default, since there is no support for saving an entity type and copying it etc. Any significant change to an entity should probably be a new type defined in the code.
The built in editor focuses mostly on entities. Using the editor you can place entities and change certain properties about them. This is mostly for visually placing the entities and changing some simple parameters. WorldArea entities are place here too, which correspond to the world's loading regions.
The entity editor. The bottom left is the entity palette for spawning new entity instances. The top right is the properties panel. The top left panel is just some debug view/buttons.
The editor includes a built in tilemap editor. This is probably the most important part of the editor for building the world. Using this, you can place tiles, animated tiles, tile properties(such as room ID, jump/fall properties and other stuff), and auto tiles. The tilemap system makes use of multiple layers so different tiles can be composited together. Once the tilemap is complete, it can be saved and displayed as an EntityVisual.
The tilemap editor. The bottom left is the palette panel where you can select tile regions to draw. Above is the layer panel for manipulating and selecting layers. To the top right is the property panel for defining which bitmap assets the tilemap will use.
When I want to test the game, I press start a test game instance(play button, F5, or F6 to start at the mouse position) and a new instance of the game will be started. This is completely separate from the editor game data and any changes here will not be reflected in the editor.
Some Other Technical Details
The code makes heavy use of code generation. Structs and enums can be annotated using macros to define meta data. For saving editor data, the game uses a general serialisation method using this system. The editor also uses this for displaying lists of enums, and drawing the property panel.
The physics system uses GJK for overlap tests and the method described in this paper for continuous blocking collisions.
Different layers
The game uses different layers for drawing separate "levels" in the game. This scales down lower levels and makes it dimmer. The player can fall down in real time, but this still only uses 2D collision detection. In terms of collision, objects on different layers just ignore each other.
Conclusion
Hopefully this gives you some idea of what the game is like and what it's like developing it. A lot of the tools/code is pretty janky at the moment. For example, the code generation tool does literally the bare minimum to output what it needs to and often breaks for random reasons. There are some big problems like the entity data structure not doing any compression at all. Right now entities are around 5kb each at all times, which is way too much.
At the moment I'm developing the first major boss in the game using just procedural animation(just animating rigid sprites). Most of the first area is complete and a few enemies have been designed and implemented. As I said above, I'm completely new to art and design, so it probably takes much longer than usual.
Thanks for reading! If you want me to go into more depth about anything mention in this post, or about the game in general, let me know. Otherwise, I'll probably just post about whatever I want or just what I'm working on in that moment.