-
Notifications
You must be signed in to change notification settings - Fork 0
Game Spec
This technical specification is purely for the initial prototype version of the game. The final product may have more features than what is described here. Further features will be described in separate technical specification documents. This specification aims primarily to describe the implementation of the code, methods of development, and scope of the project.
We are building an RPG dungeon-crawler game with overworld, dungeon and battle mechanics. Overworld and dungeon should allow movement. Battle screen should have buttons which the player may navigate with either mouse or keyboard, and these buttons should react to actions. The player should be able to save their progress, and later load said save file to restore their progress. The player should be able to access their inventory and quest menus. Battle screen should respond to items in player's inventory.
The game code will be responsible for using engine's rendering and low-level capabilities together in order to create an actual gaming experience. The game must manage logical entities, game logic, events, and GUIs.
- Entity: a game object, a fundamental class for in-game objects visible on the screen
- Protagonist: the entity which the human playing the game controls (also sometimes referred to as pawn)
- Player: the human playing the game
- Enemy: an entity which appears on the battle screen, and which the player may interact with
- Battle screen: a special procedurally-generated level, which disables normal movement, and is controlled largely via GUI
- Overworld: the main level of the game
- Dungeon: a special level of the game containing enemy encounters and a boss
- Quest: a sequence of goals, which the Protagonist may sequentially complete to proceed to the next goal in the sequence
- Item: a formal data structure holding information about an object, which appears in the inventory and may be interacted with
- Inventory: a formal data structure holding lists of items within it
- The game MUST be a fully functioning RPG, and its code MUST allow further features to be added onto it in the future
- Protagonist MUST be able to move, enter dungeons and fight enemies within dungeons
- Protagonist SHOULD be able to interact with NPCs and interactable entities
- Protagonist SHOULD be able to receive quests upon reaching a trigger, such as selecting the correct option in a dialogue
- Protagonist SHOULD be able to see dialogues, and make rudimentary choices within dialogues
- The code of the game MUST be written with principles of data-oriented-design.
- The code of the game MUST have no major bugs or issues (such as hardlocks, softlocks or features being unavailable due to glitches)
- The code of the game MUST be easily readable, well-formatted and well-structured (
clang-formatSHOULD be used)
These are things that are out of scope of the game prototype, but may still be implemented later on.
- Any features or abilities other than those that are described in the goals or are directly required to accomplish the goals SHOULD NOT be implemented
- Any quality of life tools or features which are not directly required for implementation of the goals SHOULD NOT be implemented
- Prototype's goal is to be a tech demo of the primary game features, nice looks and aestetics MUST NOT be a focus
- Prototype of the battle system only needs one enemy type, further ones MUST NOT be implemented
- Prototype of the dialogue system may only need one interactable NPC type, further ones MUST NOT be implemented
- Prototype of the quest system may only need one quest, further quests MUST NOT be implemented
- Prototype of the battle system may only need two weapons, further weapons MUST NOT be implemented
We have considered using Raylib for the rendering, however later on Vulkan was chosen instead, despite the technical difficulties writing code in it. We have had confidence that we will be able to meet all of the required deadlines, and we had a wish to learn and experiment with low-level rendering APIs. A fully complete renderer was written within a few weeks of learning and programming. This presented many challenges in memory and resource management, but the design goal of the engine is to abstract all of that away for the game code. The game will not interact with Vulkan directly, and instead would only inform the engine about what needs to be rendered, but not how.
We believe in data-oriented-design. It makes the code more verbose and clunky, however it also makes it fast by utilizing the CPU cache more efficiently, and using only the types and functions that are known to the compiler at compile-time. This doesn't mean that we can't use dynamic dispatch, it just means that we'll prefer static dispatch, composition and writing more verbose updating code. Dyndispatch is strictly prohibited in any events that run more often than once a second. Since one of the goals is to write optimized and robust code, and since this entire project is a learning opportunity, this approach was chosen.
Finally, the assets will simply be files on the filesystem, which means our designers can simply edit those and modify the layout of levels or positioning of triggers or visual appearance of things. Levels will use simple .json files generated by Tiled map editor. This should enable easy collaboration between designers and developers.
All in all, we are not taking the easiest approach to solving the problem, but we are taking an approach that will teach us a lot, since our collective wish is to learn more, learn the basics, code in the most abstraction-less way possible, where data is at the forefront of our scope. We believe that by learning that, we will more competently understand data layouts in software, and be able to write more abstracted code in a more efficient way than we otherwise would have been able to.
- Overworld, where:
- Player can use input to cause the Protagonist to move
- Player can save and load their progress
- Player can access the Protagonist's inventory, use items, equip items, throw items away
- Protagonist can interact with NPCs to access shops or gain quests or collect quest rewards
- Protagonist can enter dungeons
- Dungeons, where:
- Player can use input to cause the Protagonist to move
- Player cannot save their progress
- Player can access Protagonist's inventory by using the inventory key, and use/equip items
- Protagonist can trigger battle encounters
- Protagonist can trigger a game over
- Protagonist can exit the dungeon by leaving it. If the Protagonist leaves, their progress is reset to how it was before they entered
- Protagonist can fight the dungeon boss for extra rewards, which triggers dungeon completion, saves progress, and exits the dungeon
- Battle screen, where:
- Player can click special interaction GUI buttons (e.g. ATTACK, DEFEND, INVENTORY, EXIT)
- Player can access the Protagonist's inventory by clicking the INVENTORY button
- Player can click the ATTACK button, followed by choosing the enemy to attack, followed by choosing which limb of the enemy to attack, to queue up an attack action, which the Protagonist will perform
- Player can click the DEFEND button, followed by choosing the Protagonist's limb to defend, to queue up a defend action, which the Protagonist will perform
- Once actions are queued up, the player may click the EXECUTE button to cause the Protagonist to perform all of the desired actions
- Protagonist can trigger a game over by losing all of their health
- Player can defend the Protagonist from enemy attacks by clicking quick-time events in the correct sequence
- Player can click an exit button to leave the encounter for a penalty
- Quests system, consisting of a list of all quests the Protagonist has or ever had, each quest has a tracker for its progression/stage.
- Quest targets should be highlighted in some way in the overworld or dungeon
- Failed quests should be displayed on the list in red
- Completed quests are greyed out
- Quests should give rewards upon completion
- Quests may be completed either automatically upon meeting the criteria, or after talking to an NPC which gave you the quest
- Items system, where items may have a different type and different properties. There may be quest items, weapons, upgrades or healing items. Each item type should have its own properties, for instance, a weapon item should have a sprite, damage and energy stats.
- Inventory system, allowing player to see a list of all of the Protagonist's items. Player should be able to navigate up and down in the list, and a preview of the item they are viewing will be displayed to the side, along with item stats, description and sprite. They may equip the selected item into an equipment slot by pressing the interact key whilst hovering over the item.
- Equipment system, where the Protagonist has certain slots, where items may be equipped. Each slot accepts only items of a certain type. There may be multiple slots of the same type, perhaps depending on game progression.
- GUI system, which should consist of a GUI controller holding information about GUI elements, and simple entities with special GUI-based renderables.
- Save state, where global variables will be written, which are then serialized into a save file.
- Level scripting system, which should be a simple JSON string with nodes, which may trigger certain pre-defined events in the engine.
Currently the engine is already implemented to the point where it can render things and it does so in a fairly optimized manner. It will, prior to the beginning of implementation of game features, also be able to render text and GUI elements in a very optimized manner. It will also be able to scale the internal resolution with window resize events, and will also support fullscreen mode (both borderless window and exclusive).
The engine is already able to load assets and search for them in multiple folders. It will be able to cache the assets to minimize the amount of filesystem accesses, since most things will probably be re-used more than once in a level.
The engine will be able to load sound assets and play sounds in an optimal way, where the game would simply tell it which sounds to play and for how long.
The overworld should be a persistent tile map. Upon loading, the system should iterate over each tile and run any "scripts" attached to them, which can potentially modify each tile depending on global variables. Certain tiles will need to be updated more often, and will be marked as such. These tiles will either be updated in the update event, or update_lazy event. Upon the initial iteration, the system should also adjust each tile's position, scale, and rotation.
As the Protagonist may walk behind certain tiles, it'll be imperative that renderables are Z sorted. Each renderable will hold a reference to the entity it belongs to, so even if sorted they are not just lost. The renderables should be ordered based on their Y coordinate. Things with smaller Y coordinate have lower priority. The Z axis could also be used for additonal Z sorting of things like text.
Player may cause the Protagonist to interact with objects by pressing the interact key (which should send Cmd::USE). Upon receiving this command, iterate over the list of entities, find those that collide with a rectangle in front of the Protagonist, and of those find the first which can be interacted with. Upon finding the interactable, execute the level scripting code associated with it (such as loading a dungeon level, triggering a dialogue, etc).
Largely similar to the overworld, except that saving the game should be blocked. Scripting of the entities and updates are the same as in overworld. Z sorting is the same as well.
The largest differences in the dungeons relate to the battle system. Upon winning the battle, the Protagonist should be returned back to where they were in the dungeon, and the trigger they triggered should be disabled. If battle is lost, the Protagonist is teleported to the overworld and reset to the state they were in before they entered the dungeon.
Fleeing the dungeon results in the Protagonist being reset to the state they were in before they entered the dungeon. Which involves syncing the save state (which is not allowed to be written during the dungeon) with the current game state, in the direction save->game.
Upon entering a trigger which triggers a battle encounter, the current world is unloaded and a special battle world is loaded (to mask the loading, we may use a fade-in fade-out system). The battle world is procedurally generated and is defined in the code. Upon entering, it reads the variables in the current game state, which should contain a list of enemies being fought, their stats and other data about the enemies. Based on that data, enemy entities are placed on the screen.
If the Protagonist has no weapon equipped, their inventory should be iterated and the first available weapon should be equipped. If Protagonist's inventory has no weapons, and no weapon is equipped, automatically equip a "finger gun" weapon, which is a weapon with infinite durability but very low damage. The "finger gun" is intended as a last resort fallback if the Protagonist has no other weapon.
Then, all of the GUI elements are created, which are themselves just normal entities. Some of them may have flags that they are supposed to react to input. Depending on whether they are in focus or not, whenever the game receives directional commands (Cmd::FORWARD Cmd::RIGHT Cmd::BACK Cmd::LEFT Cmd::CLICK), the UI elements should react based on a list in the GUI controller. More detailed information about the GUI system will be provided in its own section.
Upon successful initialization, the battle screen begins animating (if animations are present). The player may choose an action by selecting or clicking a button. Each action will be defined in the code. Available actions include: attack, defend, inventory, exit. One special action appears if player spent any action points, which is a button to execute all of the queued-up actions.
Upon clicking the attack button, the GUI should change to the attack GUI. The attack GUI should present the enemy sprites to the player, with button "targets" on each of the available limbs of the enemy. Each "target" is simply a GUI button with a special sprite. The player should also have a button with their currently equipped weapon, which they can click in order to change it, by opening the inventory GUI and allowing the player to choose a new weapon item. Upon selecting or clicking a limb, the attack action is registered into the action buffer (with statistics based on the currently equipped weapon stats), action points are deducted, player returns to the main battle menu.
[todo: write the rest of this]
Items are defined using their own classes, which composites a base "Item" class as a class member. The base Item class would have information shared between all of the items, while specific item category classes would have information pertaining specifically to that item category. The basic item class would have the following:
- name: string
- description: string
- sprite: string
- price: u32
- protected: bool
- sellable: bool
- unique: bool
- max_stack: u32
Other item category classes would be defined like so:
class QuestItem {
private:
Item base;
public:
QuestItem() {
this->base.unique = true;
this->base.sellable = false;
}
};
class WeaponItem {
private:
Item base;
u32 damage;
u32 ap_cost;
};
class HealingItem {
private:
Item base;
u32 heath;
};
class BoosterItem {
private:
Item base;
u32 damage_boost;
u32 ap_boost;
u32 health_boost;
};Inventory would simply be a data structure holding a list of all items the Protagonist currently has, and have methods to manipulate data in that data structure. It would be responsible for sorting items, ensuring uniqueness and other constraints of the items, counting items of each type, and preparing data for display in the GUIs. It will also be responsible for translating item data strings into the data structures.
Inventory would have its own GUI, which can be accessed by pressing the inventory key (Cmd::INVENTORY). The GUI should appear like a list of items the Protagonist currently has and the amount of it, formatted as (Amount) Name, for example (10) Arm Repair Kit.
When inventory is open, the Protagonist cannot move. Instead, movement keys are used to navigate the inventory menu. Alternatively, the player may use mouse and mousewheel to navigate the menu. Upon selection of an item, its sprite, name and description is displayed to the side of the list. If an item is a weapon or any other item which has statistics, these statistics will be displayed there as well.
If player presses the interaction key on the item, or clicks it, a small menu would be displayed with buttons to interact with the items depending on during which stage of the game the inventory panel is opened.
The inventory panel must be openable at any stage of the game except for main menu, paused state or game over state.
Equipment should be an extension of the inventory GUI, implemented as a separate tab. It should be presented as the Protagonist sprite with slots for attachments. Slots display the sprite of the item equipped there, and if selected with either keyboard or mouse, display the stats of the item.
GUI will be implemented by using a GUI controller. There may be multiple GUI controllers existing at once, but only one may be focused at a time. A GUI controller is responsible for releasing focus to another controller once it's done being in focus.
A controller holds a list of all UI items it's responsible for, it collects input, detects which UI element the input relates to, and changes UI elements based on that. The UI elements are simple renderables, the controller is what's really responsible for their logic. The GUI controller stops performing its logic if it's unfocused, instead performing a much slower update cycle.
Save state is serializable (writable to disk) data structure, which holds important globals, statistics and information. It exists separate from the game state, and is only written with data that should be saved. It's not supposed to be modified at all during dungeon and battle phases. It's written to the disk only when player triggers a save game.
The save state may only hold basic variable types, such as numbers. It may not hold pointers, or any data structures more complex than basic types. Here is a list of types that is allowed to be held in the save state:
- All numeric types (
i8 i16 i32 i64 u8 u16 u32 u64 f32 f64) - Booleans (
bool) - Characters (
char) - C strings, terminated by NULL (
\0) (char *) - Arbitrary data structures defined by
SaveUserdatastructure
struct SaveUserdata {
void *data;
u64 length;
};When adding variables to the save state, they must be added to the serializer as well. The order of serialization/deserialization matters here!
Loading a save state involves reading the binary file and deserializing it using the process opposite of serialization. The loaded data is then written to the game state. It should always be written to the game state, and only written to the save state when we're able to save.
Levels should have an ability to include rudimentary scripts with game objects. These scripts will use simple Lua to enable level designers to create more complex level interactions than they would otherwise be able to. Here is an example of a Lua script that would be included with a trigger tile:
function ENT:interaction()
if self.clicks_count > 5 then
game.display_dialogue("You have clicked 5 times, you should stop!", "john_doe")
else
self.clicks_count = self.clicks_count + 1
end
endThese scripts will be included with each object using Tiled editor's data feature. Lua scripts will be the "script" field of Tiled objects, levels and such.
Further documentation, along with extensive amount of examples, will be provided to the level designers.