Skip to content

Distractions and Design Docs

I'm obviously at a pretty low point right now. I'm pretty well aware of my generally low energy and motivation levels, which can likely be attributed to some undiagnosed depression issues - but this is not the same.

In the last nine months, I've lost several family members, starting with my brother in December. We were very close, and I haven't been handling it well. Even before that, my dad was already experiencing health issues, and I've had consistently high levels of anxiety, believing that any day could be his last. Obviously his last day finally arrived, and in spite of all of the build up and anticipation, I was still not prepared for his death.

When things get rough, I tend to distract myself. I'm not sure if this is a way to avoid actually dealing with the situation, but my distractions are generally pretty productive. Each chapter I've written here is a product of brain distracting itself.

While I continue to ignore my obvious mental health issues and avoid coping with the inevitability of death, I'll keep trying to come up with game design ideas.

15.1 The Design Doc

I am obviously not a game designer (not yet anyway), which is why this entire process has been so difficult for me. Over the years, I've consumed a lot of content aimed at game designers, and for whatever reason, it just doesn't "click" with me. One of the pieces of advice that is often offered through this content is that you should create a "design document" that describes every aspect of your game. The document serves as a reference for the actual construction of the game. If there is any unanswered question or ambiguity in the design document, then the document should be updated to clear things up. I've attempted writing a couple of design docs over the years, but I've never actually built a game based on one.

Outside of the video game industry, the closest thing to a design doc that we have is the "waterfall" methodology, where an entire product or feature is planned out from the very beginning. In modern software development, waterfall has largely fallen out of style in favor of "agile" practices. In an agile company, we don't really have design docs - we have "product owners" or "product managers" that just tell the programmers what they want. These wants get converted into stories, which may further get broken down into tasks or grouped into epics by the developers. The developers just work on whichever story happens to be the highest priority. This process is continuous so that the relative priorities of the stories change over time, and the product evolves as the priorities shift.

I hate both of these processes, but I'm not sure if it's because I actually hate the processes themselves, or if I just hate working on somebody else's vision. In any case, I've been neglecting to use any process for building this game. Instead, I've been spending a lot of time building prototypes that I'm not even sure are good ideas to begin with.

I want to argue that writing my thoughts out at all is a form of design doc, but I know that's simply not true. What does a design doc even look like for a project like mine?

I'm obviously pretty proficient with Markdown files, so that seems like a reasonable choice. I considered publishing it on the site, but I'd like for any historical revision of the document to be easily discoverable. With that in mind, I could just create a DESIGN.md file in the game's Git repository. As I post links to the relevant commit history, the document would also contain the intended design of the game at that same point in history. I think I'll do that.

In this way, the design doc itself feels like a part of the source code of the game. I generally dislike written documentation because it inevitably gets out of sync with the actual code. In this case, however, the document isn't describing the code, rather, the code is implementing the design within the document.

If the document is considered a form of source code, then I suppose I should paste some snippets as I go.

DESIGN.md

# Wisps and the Wails

## Overview

Healing-focused game where players must overcome uniquely challenging encounters using a party of up to 5 "wisps", each with their own attributes and characteristics.

## Target Platform

PC (Steam)

## Target Audience

MMORPG players who are familiar with healing-centric gameplay

## Gameplay

- The player starts the game by selecting one of three wisps.
- Each wisp has its own passive offensive capabilities, and provides the player with an active ability.
- A wisp's offensive power is proportional to its current health level. If a wisp reaches 0 health, then it may no longer attack.
- The game is separated into several "worlds", each containing various "levels".
- Each world contains a specific theme, and every level within a world explores that world's theme.
- The player must complete each level by defeating the Wail of that level. Each Wail encounter is unique, and the player must execute the mechanics of the encounter while keeping the party healthy using active abilities.
- Completing a level rewards the player with a random wisp. If the player already has 5 wisps in the party, then an existing wisp may be replaced, or the new wisp may be passed.

## Controls

Movement: WASD

Ability Selection: QERFC

Target Selection: Mouseover

I didn't get very far into writing the document before I found myself faced with the task of assigning a theme to the game. I'll move forward with the concept I described back in chapter 11 - "Prototyping Part 4" - in which the player collects wisps that passively fight on the player's behalf, and the player's responsibility is to keep them from dying.

Eventually I'll want to go into more detail about the mechanics, like the different abilities or stats that wisps might have, or even a mechanical breakdown of each boss encounter. For now though, I think this is sufficient to get me moving in the right direction.

15.2 Text Rendering

The player starts the game by selecting one of three wisps.

An obvious callback to the Pokémon series, this gives the game a bit of variety from the very beginning. There's just one major issue: how does the player know which one to choose? This problem extends to the entire game - the player must understand the abilities that they have available to them in order to succeed.

Rendering text is actually pretty annoying. Fonts define each glyph separately as a set of curves, but parsing such a format from a shader is no easy feat. The Slug library has managed to do a wonderful job, but it's unfortunately not free, and the cost of licensing is unclear. One of the benefits of writing your own engine is that you don't have to deal with licensing fees, and I'd hate to give that up.

FreeType, on the other hand, is a free option that simply rasterizes glyphs to a bitmap so that they can be loaded into textures, which can be sampled from a simple shader. This option is nice because it can work with virtually any font. The downside (when compared to path rendering) is that a separate bitmap needs to be generated for different sizes of text, and rasterized text is susceptible to blurring or pixelization when scaled.

There is another option that is remarkably simple, but somewhat tedious: just generate our own bitmap along with some metadata about each glyph (like where it is located within the bitmap).

Creating a Font

Fonts are weird, and the licenses for them are even weirder. Software licensing is already overly complicated and full of legal gray areas, but for whatever reason, something about purchasing a license to make your text look different just doesn't make sense to me.

I started to search for fonts that used very permissive licenses, but I ran into a lot that were blatant ripoffs of other fonts that were not as permissive. I won't call them out specifically, but this is the type of thing that doesn't make sense to me. If I were to use one of these fonts without realizing it was a copycat, could the owner of the original font come after me?

Whatever. I'll just make my own font. How hard could it be? I'll just fire up Photoshop, create a bunch of adjacent 8x8 cells and whip up some pixelized glyphs.

Initial Font Attempt

I'm not gonna lie, that took me like all day, and I'm still worried that it might be too similar to something that already exists. I promise I drew them all from scratch with no references! There are only so many ways to create readable characters within an 8x8 space. There's still a ton of room for improvement - for example, the capital letter "O" and the zero are identical - but this will do just fine for now. I've saved the file as a BMP and put it in a new fonts/ subdirectory within the assets/ folder.

Loading the Font

Before I get to actually using the font, I whipped up a new render feature called TextFeature, which contains a model matrix, the desired color, and a std::string of text that should be displayed. I also created a new Text component, which contains a pointer to a TextFeature and a pointer to its Renderable.

Starting with the Metal rendering implementation, I copied the ColoredFeatureRenderer to a new TextFeatureRenderer. Rendering each character within the string will actually involve rendering multiple quads, with each one sampling a different location of the bitmap, which means each Renderable will actually be making multiple draw calls based on the size of the string defined in its TextFeature. Because of this, each character within the string needs its own model matrix, which means it needs its own uniform buffer. The text color, on the other hand, can be a single buffer that is shared by all characters within the string, so that we don't have to upload the text color to the GPU for every single draw call.

To support this paradigm, the TextFeatureRenderer contains a complicated std::vector<std::vector<std::vector<MTL::Buffer*>>> named _glyphValueBuffers. The top-level vector contains vectors for each camera; each vector within that vector contains vectors for each Renderable; finally, each of those vectors contains the MTL::Buffer for each character in the string for that Renderable's TextFeature. If the string within a TextFeature changes and exceeds the number of buffers in its own vector, then the feature renderer will dynamically allocate more buffers as needed.

Since the goal is to render the entire string, the draw() method will have a local copy of the model matrix, which we will translate by 1.0f to the right for each character that we iterate over. In theory, regardless of the position, rotation, or scale of the entity's Transform, the characters should appear one after another as intended.

However, before I can prove that, I need to learn how to actually load the font bitmap into a MTL::Texture so that we can sample it from the shader. MetalKit has a handy little MTKTextureLoader utility, but naturally it's not available from metal-cpp without some heavy modifications. I'll follow a similar pattern that we've used for loading audio files, in which the AudioManager itself isn't responsible for loading the files, but instead calls out to a provided file loader abstraction. In this case, I'll create a simple MetalTextureLoader interface with a single getTexture() method, which must be implemented by each platform. Since we only have a single texture that needs to be loaded, I'm not even going to bother with specifying which texture to load - this method will always return a texture containing our font bitmap.

alfredo/src/platform/MacMetalTextureLoader.mm

#include "MacMetalTextureLoader.h"

#include <Metal/Metal.hpp>

namespace linguine::alfredo {

MacMetalTextureLoader::~MacMetalTextureLoader() {
  for (const auto& texture : _textures) {
    texture.second->release();
  }
}

MTL::Texture* MacMetalTextureLoader::getTexture() {
  const auto filename = "font.bmp";

  auto existing = _textures.find(filename);

  if (existing != _textures.end()) {
    return existing->second;
  }

  auto url = [NSURL fileURLWithPath:[NSString stringWithUTF8String:filename]
                        isDirectory:false];

  NSError* error;
  auto result = [_mtkTextureLoader newTextureWithContentsOfURL:url
                                              options:nil
                                                error:&error];

  if (!result) {
    NSLog(@"%@", [error localizedDescription]);
    return nullptr;
  }

  _textures[filename] = (__bridge_retained MTL::Texture*) result;

  return _textures.at(filename);
}

}  // namespace linguine::alfredo

My IDE really doesn't like mixing Objective-C with C++, but oh well.

I updated the constructors and static create() methods of the Metal renderer classes so that our new TextFeatureRenderer has access to an instance of a MetalTextureLoader. The feature renderer just calls the getTexture() method in its constructor and saves the resulting pointer in an instance variable.

Sampling the Texture

With the texture now accessible from the feature renderer, it's time to update the shaders and the draw() method to sample the texture appropriately. One of the cool things about Metal is that you can apparently configure a sampler from within the shader code! In other "modern" rendering APIs, you have to configure the sampler in your application code and pass it into the shader (which can be useful if your sampler parameters change over time).

renderers/metal/src/features/TextFeatureRenderer.cpp snippet

struct MetalCamera {
  metal::float4x4 viewProjectionMatrix;
};

struct MetalColoredFeature {
  float3 color;
};

struct MetalGlyphVertexFeature {
  metal::float4x4 modelMatrix;
};

struct VertexOutput {
  float4 position [[position]];
  float3 color;
  float2 uv;
};

struct MetalGlyphFragmentFeature {
  float2 position;
};

VertexOutput vertex vertexText(uint index [[vertex_id]],
    const device float2* positions [[buffer(0)]],
    const constant MetalCamera& camera [[buffer(1)]],
    const constant MetalColoredFeature& color [[buffer(2)]],
    const constant MetalGlyphVertexFeature& glyph [[buffer(3)]]) {
  VertexOutput o;
  o.position = camera.viewProjectionMatrix * glyph.modelMatrix * float4(positions[index], 0.0, 1.0);
  o.color = color.color;
  o.uv = positions[index];
  return o;
}

float4 fragment fragmentText(VertexOutput in [[stage_in]],
    metal::texture2d<float, metal::access::sample> fontTexture [[texture(0)]],
    const constant MetalGlyphFragmentFeature& feature [[buffer(0)]]) {
  constexpr auto nearestPixel = metal::sampler(
      metal::coord::pixel,
      metal::address::clamp_to_edge,
      metal::filter::nearest
  );

  float x = feature.position.x + metal::floor(10.0 * (in.uv.x + 0.5));
  float y = feature.position.y + metal::floor(10.0 * (-in.uv.y + 0.5));
  float sample = fontTexture.sample(nearestPixel, float2(x, y)).r;
  return float4(in.color, sample);
}

This is definitely the most complex shader we've written so far, but far from the craziest I've seen over the years. The TLDR; is that the sampler is configured to return the nearest pixel, as measured in pixel coordinates (rather than normalized coordinates). Since we know each character in the bitmap is contained within a 10x10 block, we can convert our quad's normalized coordinates into pixel values between 1-10, and use a uniform buffer to tell us where to start. The sampler returns a color value of the desired pixel - 1.0 for the letters, and 0.0 for the space in between. We can simply use the sampler value as the alpha value for our output, which will allow us to support partial transparency and anti-aliasing in the future (though we'd have to use a different image format for that).

The actual draw() method contains a lot of the same logic we've seen time and time again - bind meshes and buffers as needed. We actually always use the Quad mesh, and we only bind it once, since we know all of the glyphs will use the same mesh. Similarly, we bind the font texture to the fragment shader once. Since each character has a different position within the texture, we'll need to tell the shader where to look. Some programmers store this information in a separate file, which gets loaded and parsed. I'm not sure if I'll regret this or not, but I'm just going to store the positions for all of the characters within a std::unordered_map<char, simd::float2>. For each character in the string, I'll just look up its position in the map, copy the position to the correct uniform buffer, and hope for the best.

renderers/metal/src/features/TextFeatureRenderer.h snippet

const std::unordered_map<char, simd::float2> _glyphPositions = {
    { 'A', simd::float2{ 0.0f, 0.0f } },
    { 'B', simd::float2{ 10.0f, 0.0f } },
    { 'C', simd::float2{ 20.0f, 0.0f } },
    ...
};

Everything "works", except the characters are all drawn as boxes. The problem is pretty simple: I haven't configured the color attachment to support alpha blending.

renderers/metal/src/features/TextFeatureRenderer.cpp snippet

auto colorAttachment = renderPipelineDescriptor->colorAttachments()->object(0);
colorAttachment->setPixelFormat(MTL::PixelFormat::PixelFormatBGRA8Unorm_sRGB);
colorAttachment->setBlendingEnabled(true);
colorAttachment->setRgbBlendOperation(MTL::BlendOperation::BlendOperationAdd);
colorAttachment->setAlphaBlendOperation(MTL::BlendOperation::BlendOperationAdd);
colorAttachment->setSourceRGBBlendFactor(MTL::BlendFactor::BlendFactorSourceAlpha);
colorAttachment->setSourceAlphaBlendFactor(MTL::BlendFactor::BlendFactorSourceAlpha);
colorAttachment->setDestinationRGBBlendFactor(MTL::BlendFactor::BlendFactorOneMinusSourceAlpha);
colorAttachment->setDestinationAlphaBlendFactor(MTL::BlendFactor::BlendFactorOneMinusSourceAlpha);

Text Feature Renderer in Alfredo

Getting this to work with Scampi is just a matter of creating a suitable IosMetalTextureLoader that uses the alternative [NSBundle URLForResource: withExtension:] method, just like we did for loading audio files. The current level is not designed for mobile devices, but it does at least work!

Text Feature Renderer in Scampi

Unfortunately, getting this to work in the browser requires an entirely new feature renderer within our OpenGL implementation. The actual OpenGL commands to do so are relatively simple, but OpenGL does not provide convenience methods for loading images into textures without importing separate libraries.

I did a bit of research into parsing the BMP file myself, but decided to simply download stb instead. I downloaded the current master branch as a ZIP archive and extracted it into my third_party/ directory. After including stb in Pesto's CMakeLists.txt, I implemented the WebOpenGLFileLoader to load the font.bmp image using the stbi_load() function - much easier than the file parsing rabbit hole I was about to go down.

pesto/src/platform/WebOpenGLFileLoader.cpp

#include "WebOpenGLFileLoader.h"

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

namespace linguine::pesto {

render::OpenGLFileLoader::ImageFile WebOpenGLFileLoader::getImage() const {
  auto filename = "/assets/fonts/font.bmp";

  int width, height, components;
  auto data = stbi_load(filename, &width, &height, &components, 0);

  auto size = width * height * components;
  auto dataVector = std::vector<std::byte>(size);

  memcpy(dataVector.data(), data, size);
  stbi_image_free(data);

  return ImageFile {
      .width = width,
      .height = height,
      .data = dataVector
  };
}

}  // namespace linguine::pesto

The actual shader code to sample the texture is relatively simple, after fiddling with it a bit. OpenGL samplers don't have the ability to sample pixel positions, so I've hard-coded the width and height of the texture into the shader for now. I'm not overly concerned with passing the dimensions in as another uniform variable until I have a reason to do so.

renderers/opengl/src/features/TextFeatureRenderer.cpp vertex shader

#version 300 es

layout(location = 0)
in vec2 inPosition;

out vec2 uv;

uniform mat4 viewProjectionMatrix;
uniform mat4 modelMatrix;

void main() {
  gl_Position = viewProjectionMatrix * modelMatrix * vec4(inPosition, 0.0, 1.0);
  uv = inPosition;
}

renderers/opengl/src/features/TextFeatureRenderer.cpp fragment shader

#version 300 es

precision highp float;
precision highp sampler2D;

uniform vec3 color;
uniform sampler2D sampler;
uniform vec2 position;

in vec2 uv;

out vec4 outColor;

void main() {
  float x = (position.x + floor(10.0 * (uv.x + 0.5)) + 0.5) / 160.0;
  float y = (position.y + floor(10.0 * (-uv.y + 0.5)) + 0.5) / 50.0;
  float a = 1.0 - texture(sampler, vec2(x, y)).r;
  outColor = vec4(color, a);
}

Lastly, I had to enable alpha blending in the OpenGLRenderer.

renderers/opengl/src/OpenGLRenderer.cpp

glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Text Feature Renderer in Pesto

It's not the cleanest looking text in the world, but it totally works. We can make better looking fonts and make the rendering crispier by using signed distance fields, or ensuring that the rendered texture's pixels are aligned with the pixels of the surface - we know that each quad contains a 10x10 portion of the texture, and the surface is 1440x768, so we could configure our UI camera's height and our quad's scale accordingly. I'll go ahead and change our UI camera's height to 768.0f so that a scale value of 1.0f is equivalent to 1 pixel (for elements rendered by the UI camera), rather than 1.0f being equivalent to the entire height of the surface.

After a little bit of math, I converted all of the positions and scales of UI elements to be in terms of pixels. I'm not sure if I'll leave it this way in the long term, but it is handy for now, and definitely looks better - you can check out the results here.

15.3 Choosing a Wisp

I have some doubts about building out a level where you choose your first wisp. I need to be careful about how it's designed so that I don't accidentally start building an entire world for an RPG. However, the reason I think this should be the next step is so that I can build out a lot of the core systems to support a dynamic party, such as abilities that change depending on which wisps are present in your party. It will also give the player a safe place to move around and learn the basic controls before diving into boss encounters.

My idea is for the player to encounter three wounded wisps of different types. Approaching each wisp will bestow that wisp's ability to the player, allowing the player to heal the wisp. This will hopefully serve as a tutorial for the core mechanic of the game in a safe setting - the wisps are already hurt, but they cannot die. Allowing the player to only choose one of the wisps is somewhat of an arbitrary limitation, but I can come up with a story-based reason for it later.

The Ability System

So far, we've only implemented two abilities: both instant-cast heals - one single-target heal triggered by selecting a health bar, the other a multi-target heal triggered by selecting the ability itself. We actually got rid of the multi-target "big heal" button a while back, but its behavior is still implemented within the PlayerControllerSystem.

The PlayerControllerSystem is responsible for binding the player's inputs to the correct actions. Unfortunately those actions are also encoded into the system, which makes it very rigid. I'd like to build an ability system that is more extensible. The PlayerControllerSystem should simply trigger the currently "equipped" abilities, whatever they might be.

I created a Spell struct within a new spells/ subdirectory within Linguine's data/ folder. The first couple of values I defined are no-brainers: the spell's castTime and its cooldown. The next thing that came to mind was the spell's resource cost, but we haven't added any sort of resources into the game yet, so I'll skip that for now.

Generally, I'd create a bunch of derived types inheriting from Spell, each with its own overridden function to perform its action. However, using subtypes and polymorphism requires the use of pointers, which our ECS architecture does not handle particularly well - our Renderable pointers have to be cleaned up within the entity's removal listeners, which could be an option here, but I'd like to figure out something a little cleaner. Maybe we could create a spell database that contains an instance of every spell, and our entities would just contain pointers to the spells within the database. The database would be responsible for cleaning up all the memory when it is no longer required, after our systems no longer utilize the pointers (when the game is closed). I worry about the memory overhead of keeping every spell in memory all the time - maybe each entity could just contain a spell ID, and the database could lazily construct an instance of it on-demand.

Enough theorizing, let's define some of the abilities that we want to support and write whatever code we need to support them.

Instant and Average

This will be a tweaked version of the primary heal we've been using this whole time. This ability will have no cast time, but we will add a short cooldown so that the player can't spam it. I'd like its power level to be "medium", but I'm not really sure what that means without the context of how much health each wisp has, or how much damage the bosses deal. Honestly I'm just making up the numbers as I go and we can turn some knobs later.

  • Cast time: Instant
  • Cooldown: 6 seconds
  • Heal Amount: 175

Slow but Powerful

This ability will have somewhat of a long cast time in which the player cannot move, but heal for a large amount when successfully executed. Furthermore, there will be no cooldown, so the player is free to cast it as often as they can, as long as the situation allows for it.

  • Cast time: 2.5 seconds
  • Cooldown: None
  • Heal Amount: 250

Weak Up Front, Powerful Over Time

In WoW, this type of heal is known as "healing over time" (a "HoT"). This ability will have a short cast time and no cooldown. Its healing is distributed as "ticks" over a long period of time - each tick is small, but the total amount of the ticks over a long duration makes it very powerful. Furthermore, the ability to have the HoT "rolling" on multiple targets at the same time makes it even more powerful. However, if the player waits too long to apply the HoT, the incoming damage might be too great for the ticks alone to keep their party alive, adding a bit of risk/reward gameplay.

  • Cast time: 1.5 seconds
  • Cooldown: None
  • Heal Amount: 50 every 2 seconds over 12 seconds

The first abstraction I made is an Action, with a single execute() method that requires the current EntityManager. I created a Heal subclass, which takes in a power, and should be sufficient for both the instant heal and the hard-casted heal.

linguine/src/data/spells/actions/Heal.cpp

#include "Heal.h"

#include <glm/common.hpp>

#include "components/Health.h"
#include "components/PlayerTarget.h"

namespace linguine {

void Heal::execute(EntityManager& entityManager) {
  entityManager.find<PlayerTarget, Health>()->each([this](Entity& entity) {
    auto health = entity.get<Health>();
    health->current = glm::min(health->current + _power, health->max);
  });
}

}  // namespace linguine

Next I created the SpellDatabase class, which really just contains a std::unordered_map of spell IDs to Spell instances, and a "getter" method to return a spell by its ID. I created a SpellId enum so that I could refer to spells by name, but I haven't actually named them, and so I got rid of it.

The map currently contains entries for the instant heal and the hard-casted heal, but I'll need to create a new type of Action for our HoT. I'll create an Apply class, which will be responsible for applying arbitrary effects to a target. How this effect application should work isn't immediately obvious. I considered just adding a component of the effect to the targeted entity, but it's not clear how the constructor for the Apply class could take in the type and arguments of the component that should get applied.

I ended up creating an Effect abstract class that just contains a _duration and a virtual tick() method. I also created an EffectTracker component that contains a reference to the Effect, a target ID, a "tick" counter, the amount of time since the last tick, and the amount of time since the effect's application. The driver of these effects is a new EffectSystem, whose update() method is responsible for updating any Progressable render features for that effect, and whose fixedUpdate() method actually increments the timers, calls the tick() method, and destroys the entity once the effect's duration has completed.

linguine/src/systems/EffectSystem.cpp

#include "EffectSystem.h"

#include "components/EffectTracker.h"
#include "components/Progressable.h"

namespace linguine {

void EffectSystem::update(float deltaTime) {
  findEntities<EffectTracker, Progressable>()->each([](const Entity& entity) {
    auto effectTracker = entity.get<EffectTracker>();
    auto progressable = entity.get<Progressable>();
    progressable->feature->progress = 1.0f - effectTracker->timeSinceApplication / effectTracker->effect.getDuration();
  });
}

void EffectSystem::fixedUpdate(float fixedDeltaTime) {
  findEntities<EffectTracker>()->each([this, fixedDeltaTime](Entity& entity) {
    auto effectTracker = entity.get<EffectTracker>();
    effectTracker->timeSinceLastTick += fixedDeltaTime;
    effectTracker->timeSinceApplication += fixedDeltaTime;
    effectTracker->effect.tick(getEntityManager(), effectTracker);

    if (effectTracker->timeSinceApplication >= effectTracker->effect.getDuration()) {
      entity.destroy();
    }
  });
}

}  // namespace linguine

I then created a new HealOverTime effect, which contains a number of ticks and a power level per-tick, in addition to the base class's duration.

linguine/src/data/spells/effects/HealOverTime.cpp

#include "HealOverTime.h"

#include <glm/common.hpp>

#include "components/EffectTracker.h"
#include "components/Health.h"

namespace linguine {

void HealOverTime::tick(EntityManager& entityManager, Component<EffectTracker>& tracker) {
  auto tickDuration = getDuration() / static_cast<float>(_ticks);

  while (tracker->timeSinceLastTick >= tickDuration) {
    if (++tracker->ticks <= _ticks) {
      auto target = entityManager.getById(tracker->targetId);
      auto health = target->get<Health>();
      health->current = glm::min(health->current + _powerPerTick, health->max);
    }

    tracker->timeSinceLastTick -= tickDuration;
  }
}

}  // namespace linguine

I'm doing some feeble translation between elapsed time and a integral tick count so that I don't accidentally drop ticks because of floating point imprecision.

I updated the SpellDatabase with entries for each of the desired abilities.

linguine/src/data/spells/SpellDatabase.h

#pragma once

#include <unordered_map>

#include "Spell.h"
#include "actions/Apply.h"
#include "actions/Heal.h"
#include "effects/HealOverTime.h"

namespace linguine {

class SpellDatabase {
  public:
    Spell& getSpellById(uint64_t id) {
      return *_spells[id];
    }

  private:
    std::unordered_map<uint64_t, std::shared_ptr<Spell>> _spells {
        { 0, std::make_shared<Spell>(0.0f, 6.0f, std::make_unique<Heal>(175)) },
        { 1, std::make_shared<Spell>(2.5f, 0.0f, std::make_unique<Heal>(250)) },
        { 2, std::make_shared<Spell>(1.5f, 0.0f, std::make_unique<Apply>(std::make_unique<HealOverTime>(12.0f, 6, 50))) }
    };
};

}  // namespace linguine

To test out the actual spell actions (but not yet the spells themselves), I updated the PlayerControllerSystem to dynamically add and remove the PlayerTarget component to the desired target, query the spell database for a desired spell, and execute the desired action. The PlayerTarget component was formerly used for the target reticle entity so that it could adjust its position according to the target's movements. Since it's being repurposed to tag the target of the player's healing abilities, I've removed the ID field from it, and completely deleted the PlayerTargetingSystem and PlayerAttackSystem, which made heavy use of the PlayerTarget component. I also removed the auto-targeting logic from the ProjectileSystem, and deleted the BigHeal component. The BigHeal can be replaced by our new ability system, and we'll just have to revisit the player's offensive behavior entirely later.

linguine/src/systems/PlayerControllerSystem.cpp

#include "PlayerControllerSystem.h"

#include "components/GlobalCooldown.h"
#include "components/HealthBar.h"
#include "components/PlayerTarget.h"
#include "components/Tapped.h"

namespace linguine {

void PlayerControllerSystem::update(float deltaTime) {
  findEntities<PlayerTarget>()->each([](Entity& entity) {
    entity.remove<PlayerTarget>();
  });

  findEntities<GlobalCooldown>()->each([this](const Entity& entity) {
    auto globalCooldown = entity.get<GlobalCooldown>();

    if (globalCooldown->elapsed >= globalCooldown->total) {
      findEntities<HealthBar, Tapped>()->each([this, &globalCooldown](const Entity& healthBarEntity) {
        auto healthBar = healthBarEntity.get<HealthBar>();
        auto target = getEntityById(healthBar->entityId);

        target->add<PlayerTarget>();

        auto& spell = _spellDatabase.getSpellById(2);
        spell.action->execute(getEntityManager());

        globalCooldown->elapsed = 0.0f;
      });
    }
  });
}

}  // namespace linguine

Since I'm executing the actions directly, there is no per-spell cooldown tracking yet. However, everything does work as intended - which is awesome - and the global cooldown prevents the player from spamming abilities too frequently.

Technically, there's nothing stopping the player from applying multiple instances of the same effect to the same health bar, so let's fix that.

linguine/src/data/spells/actions/Apply.cpp

#include "Apply.h"

#include "components/EffectTracker.h"
#include "components/PlayerTarget.h"

namespace linguine {

void Apply::execute(EntityManager& entityManager) {
  entityManager.find<PlayerTarget>()->each([this, &entityManager](Entity& entity) {
    auto targetId = entity.getId();
    bool found = false;

    entityManager.find<EffectTracker>()->each([this, targetId, &found](const Entity& effectTrackerEntity) {
      auto effectTracker = effectTrackerEntity.get<EffectTracker>();

      if (effectTracker->targetId == targetId && &effectTracker->effect == _effect.get()) {
        effectTracker->ticks = 0;
        effectTracker->timeSinceApplication = effectTracker->timeSinceLastTick;
        found = true;
      }
    });

    if (!found) {
      auto effectEntity = entityManager.create();
      effectEntity->add<EffectTracker>(*_effect, entity.getId());
    }
  });
}

}  // namespace linguine

At first, I reset both timeSinceApplication and timeSinceLastTick to 0.0f, but that had the unfortunate side-effect of delaying the next tick. I'll respect the value of timeSinceLastTick and just set timeSinceApplication to the same value. The result should be a slightly shorter total duration without having delayed the next tick.

Speaking of effect durations, it would be really nice to be able to visually track how long a given effect will last. I need to add some code to the Apply's execute() method to create a new Progressable for each Effect. Unfortunately, Apply (or any Action) doesn't have access to the Renderer or ServiceLocator. My first instinct was to add the ServiceLocator as a parameter to the execute() method, but the systems that invoke the method also won't have access to the ServiceLocator. So I decided to flip those dependencies around: the SpellDatabase will depend on the ServiceLocator and EntityManager, which will be passed in by the Scene. The SpellDatabase can then pass those dependencies to any Spell, Action, or Effect that need them. From there, it was pretty simple to hook up all the required behaviors.

linguine/src/data/spells/actions/Apply.cpp

#include "Apply.h"

#include "components/EffectTracker.h"
#include "components/PlayerTarget.h"
#include "components/Progressable.h"
#include "components/Transform.h"
#include "entity/Result.h"

namespace linguine {

void Apply::execute() {
  _entityManager.find<PlayerTarget>()->each([this](Entity& entity) {
    auto targetId = entity.getId();
    bool found = false;

    _entityManager.find<EffectTracker>()->each([this, targetId, &found](const Entity& effectTrackerEntity) {
      auto effectTracker = effectTrackerEntity.get<EffectTracker>();

      if (effectTracker->targetId == targetId && &effectTracker->effect == _effect.get()) {
        effectTracker->ticks = 0;
        effectTracker->timeSinceApplication = effectTracker->timeSinceLastTick;
        found = true;
      }
    });

    if (!found) {
      auto effectEntity = _entityManager.create();
      effectEntity->add<EffectTracker>(*_effect, entity.getId());

      auto transform = effectEntity->add<Transform>();
      transform->scale = glm::vec3(64.0f, 8.0f, 0.0f);

      auto& renderer = _serviceLocator.get<Renderer>();

      auto progressable = effectEntity->add<Progressable>();
      progressable->feature = new ProgressFeature();
      progressable->feature->color = { 0.0f, 1.0f, 0.0f };
      progressable->feature->meshType = Quad;
      progressable->renderable = renderer.create(std::unique_ptr<RenderFeature>(progressable->feature), UI);
      progressable.setRemovalListener([progressable](const Entity e) {
        progressable->renderable->destroy();
      });
    }
  });
}

}  // namespace linguine

linguine/src/systems/EffectSystem.cpp snippet

void EffectSystem::update(float deltaTime) {
  findEntities<EffectTracker, Progressable>()->each([this](const Entity& entity) {
    auto effectTracker = entity.get<EffectTracker>();
    auto progressable = entity.get<Progressable>();
    progressable->feature->progress = 1.0f - effectTracker->timeSinceApplication / effectTracker->effect.getDuration();

    findEntities<HealthBar>()->each([&entity, &effectTracker](const Entity& healthBarEntity) {
      auto healthBar = healthBarEntity.get<HealthBar>();

      if (healthBar->entityId == effectTracker->targetId) {
        auto healthBarTransform = healthBarEntity.get<Transform>();

        auto transform = entity.get<Transform>();
        transform->position.x = healthBarTransform->position.x;
        transform->position.y = healthBarTransform->position.y - healthBarTransform->scale.y / 2.0f - transform->scale.y / 2.0f;
      }
    });
  });
}

The progress bar shows up as intended, but it appears that our HoT is only ticking 5 times instead of 6! This is almost certainly a floating point precision problem (even though I tried to avoid that!). Rather than destroying the entity based on the elapsed time, I've decided to destroy it based on the number of ticks. The floating-point timeSinceApplication value is still around for the sake of displaying the progress bar, but its value is not used for determining when to destroy the entity. I moved the tick value from the HealOverTime effect up to the abstract Effect class. I also took the opportunity to move the "ticking" logic into the base class, and instead have virtual onApply(), onTick(), and onRemove() methods that each effect can implement.

linguine/src/data/spells/effects/Effect.cpp

#include "Effect.h"

#include "components/EffectTracker.h"
#include "entity/Entity.h"

namespace linguine {

void Effect::update(Component<EffectTracker>& tracker) {
  auto tickDuration = getDuration() / static_cast<float>(getTicks());

  while (tracker->timeSinceLastTick >= tickDuration) {
    if (++tracker->ticks <= getTicks()) {
      onTick(tracker);
    }

    tracker->timeSinceLastTick -= tickDuration;
  }
}

}  // namespace linguine

linguine/src/data/spells/effects/HealOverTime.cpp

#include "HealOverTime.h"

#include <glm/common.hpp>

#include "components/EffectTracker.h"
#include "components/Health.h"
#include "entity/Entity.h"

namespace linguine {

void HealOverTime::onTick(Component<EffectTracker>& tracker) {
  auto target = _entityManager.getById(tracker->targetId);
  auto health = target->get<Health>();
  health->current = glm::min(health->current + _powerPerTick, health->max);
}

}  // namespace linguine

linguine/src/systems/EffectSystem.cpp snippet

void EffectSystem::fixedUpdate(float fixedDeltaTime) {
  findEntities<EffectTracker>()->each([fixedDeltaTime](Entity& entity) {
    auto effectTracker = entity.get<EffectTracker>();
    effectTracker->timeSinceLastTick += fixedDeltaTime;
    effectTracker->timeSinceApplication += fixedDeltaTime;
    effectTracker->effect.update(effectTracker);

    if (effectTracker->ticks >= effectTracker->effect.getTicks()) {
      effectTracker->effect.onRemove(effectTracker);
      entity.destroy();
    }
  });
}

Next we need to respect the configured cast times and cooldowns of the abilities. We already have a Cooldown component that we've used in the past, but I think I'm going to remove it in favor of a new Ability component, containing a reference to the spell that should be cast, as well as the remaining cooldown of the ability. The TileSelectionSystem and UnitCreationSystem were apparently using the Cooldown component, so I deleted them as well.

I had to update the old CooldownProgressSystem to update the Ability component. Since a couple of our abilities have no cooldown, I added a little conditional so that we don't accidentally start dividing by zero.

linguine/src/systems/CooldownProgressSystem.cpp

#include "CooldownProgressSystem.h"

#include "components/Ability.h"
#include "components/GlobalCooldown.h"
#include "components/Progressable.h"

namespace linguine {

void CooldownProgressSystem::update(float deltaTime) {
  findEntities<Ability, Progressable>()->each([deltaTime](const Entity& entity) {
    auto ability = entity.get<Ability>();

    if (ability->spell.cooldown > 0.0f) {
      ability->remainingCooldown -= deltaTime;

      auto progressable = entity.get<Progressable>();
      progressable->feature->progress = glm::clamp(
          1.0f - ability->remainingCooldown / ability->spell.cooldown,
          0.0f, 1.0f);
    }
  });

  findEntities<GlobalCooldown, Progressable>()->each([deltaTime](const Entity& entity) {
    auto globalCooldown = entity.get<GlobalCooldown>();
    globalCooldown->elapsed += deltaTime;

    auto progressable = entity.get<Progressable>();
    progressable->renderable->setEnabled(globalCooldown->elapsed < globalCooldown->total);
    progressable->feature->progress = glm::clamp(
        globalCooldown->elapsed / globalCooldown->total,
        0.0f,
        1.0f
    );
  });
}

}  // namespace linguine

Other than that, I just had to tweak the PlayerControllerSystem to respect the cooldown of the ability.

linguine/src/systems/PlayerControllerSystem.cpp

#include "PlayerControllerSystem.h"

#include "components/Ability.h"
#include "components/GlobalCooldown.h"
#include "components/HealthBar.h"
#include "components/PlayerTarget.h"
#include "components/Tapped.h"

namespace linguine {

void PlayerControllerSystem::update(float deltaTime) {
  findEntities<PlayerTarget>()->each([](Entity& entity) {
    entity.remove<PlayerTarget>();
  });

  findEntities<GlobalCooldown>()->each([this](const Entity& entity) {
    auto globalCooldown = entity.get<GlobalCooldown>();

    if (globalCooldown->elapsed >= globalCooldown->total) {
      findEntities<HealthBar, Tapped>()->each([this, &globalCooldown](const Entity& healthBarEntity) {
        auto healthBar = healthBarEntity.get<HealthBar>();
        auto target = getEntityById(healthBar->entityId);

        target->add<PlayerTarget>();

        findEntities<Ability>()->each([&globalCooldown](const Entity& abilityEntity) {
          auto ability = abilityEntity.get<Ability>();

          if (ability->remainingCooldown <= 0.0f) {
            ability->spell.action->execute();
            ability->remainingCooldown = ability->spell.cooldown;
            globalCooldown->elapsed = 0.0f;
          }
        });
      });
    }
  });
}

}  // namespace linguine

Of course this only works as intended when we have a single ability - if we add more, then they will all trigger. The primary reason this doesn't work is because we currently only have one input that allows the player to trigger abilities: clicking a health bar. What we really want is for each ability to be bound to a separate key.

Before we go down that rabbit hole, I'd like to respect the cast time of each spell, and prevent the player's movement while casting. Bear with me, because I think that might require a lot of changes.

I'll start by creating a Cast component that will represent the state of the current cast. As such, it will contain an optional entity ID of the ability that should be executed, as well as the elapsed time since the start of the cast. I'll create an entity in the scene with this component, as well as a ProgressFeature so that we can visualize a cast bar.

I've created a CastSystem, which will be responsible for updating the progress of the cast bar and executing the ability upon a successful cast. That means the PlayerControllerSystem is no longer responsible for actually executing the ability, just configuring the Cast so that it can be executed later. Interestingly, the Heal class utilizes the PlayerTarget component to determine which entity should be healed, but the way I'm envisioning this system leads me to believe that we should store the intended target in the Cast component, and update the execute() method to receive a target entity (rather than querying for the target later). So that's exactly what I did.

With the cast bar now tracking and executing casts correctly, it's time to prevent the player from casting and moving at the same time. Unfortunately, there's no way to tell if the player is currently moving, since the DirectionalMovementSystem reads the inputs and modifies the player's PhysicalState directly. To resolve this, we'll create a new Velocity component to store the player's current movement state. The DirectionalMovementSystem will update the player's velocity in its update() method, and adjust the player's PhysicalState based on its current velocity in its fixedUpdate() method. Other systems will be free to check the player's velocity as needed.

linguine/src/systems/DirectionalMovementSystem.cpp

#include "DirectionalMovementSystem.h"

#include <glm/gtx/norm.hpp>

#include "components/PhysicalState.h"
#include "components/Player.h"
#include "components/Velocity.h"

namespace linguine {

void DirectionalMovementSystem::update(float deltaTime) {
  auto direction = glm::vec2(0.0f);

  if (_inputManager.isKeyPressed(InputManager::W)) {
    direction += glm::vec2(0.0f, 1.0f);
  }

  if (_inputManager.isKeyPressed(InputManager::A)) {
    direction += glm::vec2(-1.0f, 0.0f);
  }

  if (_inputManager.isKeyPressed(InputManager::S)) {
    direction += glm::vec2(0.0f, -1.0f);
  }

  if (_inputManager.isKeyPressed(InputManager::D)) {
    direction += glm::vec2(1.0f, 0.0f);
  }

  findEntities<Player, Velocity>()->each([&direction](const Entity& entity) {
    auto velocity = entity.get<Velocity>();

    if (glm::length2(direction) > 0.0f) {
      velocity->velocity = glm::normalize(direction) * 5.0f;
    } else {
      velocity->velocity = { 0.0f, 0.0f };
    }
  });
}

void DirectionalMovementSystem::fixedUpdate(float fixedDeltaTime) {
  findEntities<Player, Velocity, PhysicalState>()->each([fixedDeltaTime](const Entity &entity) {
    auto velocity = entity.get<Velocity>();
    auto physicalState = entity.get<PhysicalState>();

    physicalState->currentPosition += velocity->velocity * fixedDeltaTime;

    if (glm::length2(velocity->velocity) > 0.0f) {
      auto direction = glm::normalize(velocity->velocity);
      physicalState->currentRotation = glm::atan(direction.y, direction.x) - glm::half_pi<float>();
    }
  });
}

}  // namespace linguine

linguine/src/systems/PlayerControllerSystem.cpp

#include "PlayerControllerSystem.h"

#include <glm/gtx/norm.hpp>

#include "components/Ability.h"
#include "components/Cast.h"
#include "components/GlobalCooldown.h"
#include "components/HealthBar.h"
#include "components/Player.h"
#include "components/Tapped.h"
#include "components/Velocity.h"

namespace linguine {

void PlayerControllerSystem::update(float deltaTime) {
  findEntities<GlobalCooldown>()->each([this](const Entity& entity) {
    auto globalCooldown = entity.get<GlobalCooldown>();

    if (globalCooldown->elapsed >= globalCooldown->total) {
      findEntities<HealthBar, Tapped>()->each([this, &globalCooldown](const Entity& healthBarEntity) {
        auto healthBar = healthBarEntity.get<HealthBar>();

        findEntities<Ability>()->each([this, &globalCooldown, &healthBar](const Entity& abilityEntity) {
          auto ability = abilityEntity.get<Ability>();

          if (ability->remainingCooldown <= 0.0f) {
            findEntities<Cast>()->each([&abilityEntity, &globalCooldown, &healthBar](const Entity& castEntity) {
              auto cast = castEntity.get<Cast>();

              if (cast->elapsed <= 0.0f) {
                cast->abilityEntityId = abilityEntity.getId();
                cast->targetEntityId = healthBar->entityId;
                globalCooldown->elapsed = 0.0f;
              }
            });
          }
        });
      });
    }
  });
}

}  // namespace linguine

linguine/src/systems/CastSystem.cpp

#include "CastSystem.h"

#include <glm/gtx/norm.hpp>

#include "components/Ability.h"
#include "components/Cast.h"
#include "components/GlobalCooldown.h"
#include "components/Player.h"
#include "components/Progressable.h"
#include "components/Velocity.h"

namespace linguine {

void CastSystem::update(float deltaTime) {
  findEntities<Cast, Progressable>()->each([this, deltaTime](Entity& entity) {
    auto cast = entity.get<Cast>();

    if (cast->abilityEntityId) {
      auto progressable = entity.get<Progressable>();

      auto abilityEntity = getEntityById(*cast->abilityEntityId);
      auto ability = abilityEntity->get<Ability>();

      if (ability->spell.castTime > 0.0f) {
        auto isMoving = false;

        findEntities<Player, Velocity>()->each([&isMoving](const Entity& entity) {
          auto velocity = entity.get<Velocity>();
          isMoving = glm::length2(velocity->velocity) > 0.0f;
        });

        if (isMoving) {
          cast->abilityEntityId = {};
          cast->targetEntityId = {};
          cast->elapsed = 0.0f;

          findEntities<GlobalCooldown>()->each([](const Entity& gcdEntity) {
            auto globalCooldown = gcdEntity.get<GlobalCooldown>();
            globalCooldown->elapsed = globalCooldown->total;
          });

          progressable->renderable->setEnabled(false);

          return;
        }
      }

      cast->elapsed += deltaTime;

      if (cast->elapsed >= ability->spell.castTime) {
        auto target = getEntityById(*cast->targetEntityId);
        ability->spell.action->execute(*target);
        ability->remainingCooldown = ability->spell.cooldown;
        cast->abilityEntityId = {};
        cast->targetEntityId = {};
        cast->elapsed = 0.0f;
        progressable->renderable->setEnabled(false);
      } else {
        progressable->feature->progress = cast->elapsed / ability->spell.castTime;
        progressable->renderable->setEnabled(true);
      }
    }
  });
}

}  // namespace linguine

The "global" cooldown has had its own progress bar for a while, but that was only because the primary healing ability didn't have its own progress bar. Since we'll have separate indicators for each ability, we can simply display the global cooldown on all of the ability progress bars by choosing the max time remaining between the global cooldown and the ability's cooldown.

linguine/src/systems/CooldownProgressSystem.cpp

#include "CooldownProgressSystem.h"

#include "components/Ability.h"
#include "components/GlobalCooldown.h"
#include "components/Progressable.h"

namespace linguine {

void CooldownProgressSystem::update(float deltaTime) {
  findEntities<GlobalCooldown>()->each([this, deltaTime](const Entity& entity) {
    auto globalCooldown = entity.get<GlobalCooldown>();
    globalCooldown->elapsed += deltaTime;

    findEntities<Ability, Progressable>()->each([deltaTime, &globalCooldown](const Entity& entity) {
      auto ability = entity.get<Ability>();

      auto progressable = entity.get<Progressable>();
      auto progress = globalCooldown->elapsed / globalCooldown->total;

      if (ability->spell.cooldown > 0.0f) {
        ability->remainingCooldown -= deltaTime;

        if (ability->remainingCooldown > globalCooldown->total - globalCooldown->elapsed) {
          progress = 1.0f - ability->remainingCooldown / ability->spell.cooldown;
        }
      }

      progressable->feature->progress = glm::clamp(progress, 0.0f, 1.0f);
    });
  });
}

}  // namespace linguine

Now that cooldowns and cast times are working as intended, we can finally add support for using multiple abilities using separate keybinds! I'll go ahead and commit all of the changes I've made so far - it's a lot.

Healing the units based on clicks/taps was a decision we made back when we were designing the game for mobile devices, which generally don't have physical keyboards attached to them. Now that we've pivoted to desktop clients, we can make some adjustments.

As I mentioned in the design doc, I'd like our abilities to be triggerable using the keys Q, E, R, F, and C. The player should be able to choose which unit to target by simply hovering their mouse cursor over the unit's health bar prior to pressing the keybind for the ability.

The first thing I'll need to do is add support for the mouseover state to the GestureRecognitionSystem. I'll start by creating a new Hovered component, similar to our Tapped and LongPressed components. Unfortunately the InputManager doesn't currently have a way to detect mouse cursor locations, so I'll go ahead and add a getCursorLocation() method to the interface, which will return a new CursorLocation struct, containing the normalized x and y coordinates of the cursor.

Alfredo can just use the NSEventTypeMouseMoved event to implement the behavior, Pesto can use the emscripten_set_mousemove_callback callback, and Scampi can just always return some bogus coordinates, since it would never have a cursor.

For whatever reason, the locationInWindow property of the NSEvent in Alfredo is not correct, so I had to use a different method to get the mouse's current screen position, and convert it to window coordinates.

alfredo/src/platform/MacInputManager.mm snippet

case NSEventTypeMouseMoved: {
  auto mouseLocation = [window convertPointFromScreen:[NSEvent mouseLocation]];

  if (mouseLocation.x < 0.0f || mouseLocation.x > frameSize.width
      || mouseLocation.y < 0.0f || mouseLocation.y > frameSize.height) {
    break;
  }

  _cursorLocation.x = static_cast<float>(mouseLocation.x / frameSize.width);
  _cursorLocation.y = static_cast<float>(mouseLocation.y / frameSize.height);
  break;
}

When working on Pesto's implementation, I realized that I was always considering mouse "moved" events to be "drag" events, even if no mouse buttons were held down. I added a new private onMouseDragged() method to differentiate from the onMouseMoved() method, and updated the callback to handle things appropriately.

pesto/src/platform/WebInputManager.cpp constructor snippet

emscripten_set_mousemove_callback("canvas", this, false, [](inteventType, const EmscriptenMouseEvent* mouseEvent, void* userData) ->EM_BOOL {
  auto inputManager = static_cast<WebInputManager*>(userData);

  if (mouseEvent->buttons) {
    if (mouseEvent->buttons & 1) {
      inputManager->onMouseDragged(1, mouseEvent->targetX, mouseEvent->targetY);
    }

    if (mouseEvent->buttons & 2) {
      inputManager->onMouseDragged(2, mouseEvent->targetX, mouseEvent->targetY);
    }

    if (mouseEvent->buttons & 4) {
      inputManager->onMouseDragged(3, mouseEvent->targetX, mouseEvent->targetY);
    }

    if (mouseEvent->buttons & 8) {
      inputManager->onMouseDragged(4, mouseEvent->targetX, mouseEvent->targetY);
    }

    if (mouseEvent->buttons & 16) {
      inputManager->onMouseDragged(5, mouseEvent->targetX, mouseEvent->targetY);
    }
  }

  inputManager->onMouseMoved(mouseEvent->targetX, mouseEvent->targetY);
  return true;
});

pesto/src/platform/WebInputManager.cpp snippet

void WebInputManager::onMouseMoved(long x, long y) {
  _cursorLocation.x = static_cast<float>(x) / _viewport.getWidth();
  _cursorLocation.y = 1.0f - static_cast<float>(y) / _viewport.getHeight();
}

Scampi's implementation was luckily just as trivial as expected, but I'll have to be sure not to use this method for games that I might develop on iOS in the future.

scampi/src/platform/IosInputManager.h snippet

[[nodiscard]] CursorLocation getCursorLocation() const override {
  return CursorLocation { -1.0f, -1.0f };
}

After modifying the GestureRecognitionSystem to mark the currently hovered entity as Hovered, Alfredo started crashing. I spent about an hour and a half debugging before I realized I just needed to add a waitUntilCompleted() to the command buffer in the MetalRenderer. Apparently I was attempting to copy bytes from the selectable texture before it was done rendering the previous frame.

I tweaked the PlayerControllerSystem to use the Hovered component instead of Tapped, and it works surprisingly well - the ability gets casted on whichever entity the mouse is currently hovering over, except when the player is already casting or the ability is on cooldown. It's cool that it works, but that's not the behavior that I'm going for.

I updated Alfredo and Pesto's InputManager implementations to support the additional keys that we'll be using. I had to explicitly forward the Command+Q event to the NSApplication in Alfredo, so that the "quit" key-combination would continue to work.

I also split the Key enum out into its own file, which prompted me to move it and the InputManager.h file into a new input/ directory. I added a key value to the Ability component, now that I can just include the Key enum rather than the entire InputManager. I set the key for our test ability to "E", and finally updated the PlayerControllerSystem to only cast the spell if the ability's key is currently being pressed.

linguine/src/systems/PlayerControllerSystem.cpp

#include "PlayerControllerSystem.h"

#include "components/Ability.h"
#include "components/Cast.h"
#include "components/GlobalCooldown.h"
#include "components/HealthBar.h"
#include "components/Hovered.h"

namespace linguine {

void PlayerControllerSystem::update(float deltaTime) {
  findEntities<GlobalCooldown>()->each([this](const Entity& entity) {
    auto globalCooldown = entity.get<GlobalCooldown>();

    if (globalCooldown->elapsed >= globalCooldown->total) {
      findEntities<HealthBar, Hovered>()->each([this, &globalCooldown](const Entity& healthBarEntity) {
        auto healthBar = healthBarEntity.get<HealthBar>();

        findEntities<Cast>()->each([this, &globalCooldown, &healthBar](const Entity& castEntity) {
          auto cast = castEntity.get<Cast>();

          if (cast->elapsed <= 0.0f) {
            findEntities<Ability>()->each([this, &cast, &globalCooldown, &healthBar](const Entity& abilityEntity) {
              auto ability = abilityEntity.get<Ability>();

              if (_inputManager.isKeyPressed(ability->key) && ability->remainingCooldown <= 0.0f) {
                cast->abilityEntityId = abilityEntity.getId();
                cast->targetEntityId = healthBar->entityId;
                globalCooldown->elapsed = 0.0f;
              }
            });
          }
        });
      });
    }
  });
}

}  // namespace linguine

Success! Now I just want to have multiple abilities available to the player at the same time. All I have to do is create a couple of entities in the scene with Ability components containing different spells and keys. While I'm at it, I'll add some text over the indicators to show the keybind for each ability.

Ability System

Not bad! There's still no way to actually defeat the enemy, but you should still try it out here.

Dynamic Party Makeup

The idea, as described by the design doc, is that the player's abilities should dynamically change based on the wisps currently in the party. We've pretty consistently been using 5 wisps, which is mostly because that's a standard party size in WoW. Even in raids with dozens of members, generally 1 out of every 5 raid members is a healer. Other games use different party sizes - Final Fantasy XIV, for example, generally uses a 1 out of 4 ratio instead.

I scrambled a bit with different ways of going about this, but I decided to tackle it from a UI perspective - how does the HUD decide which abilities an health bars to show? As such, I created a new HudSystem.

To start with, I added a new Party component, which will be added to the player entity. This new component just stores a vector of entity IDs, each representing what we were previously referring to as an "orbiter". Each of those entities is expected to contain the Health and Ability components. Meanwhile, the HudSystem compares the size of the Party vector to the number of health bars on the screen. If there aren't enough health bars, it creates new ones. If there are too many health bars, it destroys them. Finally, it loops over the existing health bars sets their positions so that they stay centered.

I went the same route with the ability buttons, except we had to move the Ability component to the wisp entities, so I created a new AbilityButton entity, which will mark an entity as a visual representation of a wisp's ability. The new component contains the ID of the entity that contains the Ability that should be tracked, as well as the ID of the entity that will display the keybind text. In addition to adjusting the positions of the AbilityButtons, the HudSystem also sets the keybinds and text of those buttons according to their order, which is the same order as the vector in the Party component. This way, the player will eventually be able to adjust the order of their party, and the keybinds will be dynamically updated to reflect the new order. Because of the introduction of the AbilityButton component, I had to update the PlayerControllerSystem and the CooldownProgressSystem accordingly.

I had previously hard-coded the colors of the ability buttons. As I moved the button creation logic to the HudSystem, I realized I had to either use the same color for all the buttons, or come up with a way to differentiate the buttons. I decided to add a color value to each Spell so that the color of each button changes depending on which spell is being used. Eventually I'll want to change this to specify an icon instead.

The end result of these changes looks the same, except that there are always the same number of health bars as there are abilities. The actual code isn't far off from what was previously hard-coded into the scene, so I won't post it here, but you can always check out the Git history if you're interested. In any case, here is a playable version.

Tooltips

There's currently no way to communicate to the player what each ability actually does. I'd like to add a simple way to convey how each ability works by hovering over the ability's button.

Of course, the task I thought would be relatively simple turned out to be somewhat of a headache.

I started off by making a Tooltip component and a TooltipSystem, without much indication of what values might go in the component. I figured I needed to at least create the tooltip's background Drawable and foreground Text entities, so I decided to do so lazily by storing the IDs of those entities within the component, and detecting if they had been set yet. If not, then the system goes ahead and creates them.

It seemed obvious that the Tooltip needed a string value of some sort, so I added one. That's when I realized the system would have to perform some string inspection to correctly size the background. The system iterates over each character of the string in order to calculate the maximum "line" size, as well as the number of lines. That's when I realized I didn't actually add support in the TextFeatureRenderer to render multiple lines of text. It wasn't actually that difficult to add the support - just a matter of translating the model matrix based on any "new line" characters that encounter.

renderers/metal/src/features/TextFeatureRenderer.cpp draw() snippet

auto currentModelMatrix = feature.modelMatrix;
auto lineStartModelMatrix = currentModelMatrix;
auto horizontalTranslation = glm::vec3(1.0f, 0.0f, 0.0f);
auto verticalTranslation = glm::vec3(0.0f, -1.0f, 0.0f);

for (const auto& character : feature.text) {
  if (character == '\n') {
    currentModelMatrix = glm::translate(lineStartModelMatrix, verticalTranslation);
    lineStartModelMatrix = currentModelMatrix;
  } else {
    auto glyphVertexValueBuffer = glyphVertexValueBuffers[glyphValueBufferIndex];
    auto metalGlyphVertexFeature = static_cast<MetalGlyphVertexFeature *>(glyphVertexValueBuffer->contents());
    memcpy(&metalGlyphVertexFeature->modelMatrix, &currentModelMatrix, sizeof(simd::float4x4));

    commandEncoder->setVertexBuffer(glyphVertexValueBuffer, 0, 3);

    auto glyphFragmentValueBuffer = glyphFragmentValueBuffers[glyphValueBufferIndex];
    auto metalGlyphFragmentFeature = static_cast<MetalGlyphFragmentFeature *>(glyphFragmentValueBuffer->contents());
    memcpy(&metalGlyphFragmentFeature->position, &_glyphPositions.at(character), sizeof(simd::float2));

    commandEncoder->setFragmentBuffer(glyphFragmentValueBuffer, 0, 0);

    mesh->draw(*commandEncoder);

    currentModelMatrix = glm::translate(currentModelMatrix, horizontalTranslation);
  }
  ++glyphValueBufferIndex;
}

renderers/opengl/src/features/TextFeatureRenderer.cpp draw() snippet

auto currentModelMatrix = feature.modelMatrix;
auto lineStartModelMatrix = currentModelMatrix;
auto horizontalTranslation = glm::vec3(1.0f, 0.0f, 0.0f);
auto verticalTranslation = glm::vec3(0.0f, -1.0f, 0.0f);

for (const auto& character : feature.text) {
  if (character == '\n') {
    currentModelMatrix = glm::translate(lineStartModelMatrix, verticalTranslation);
    lineStartModelMatrix = currentModelMatrix;
  } else {
    glUniformMatrix4fv(_modelMatrixLocation, 1, GL_FALSE, glm::value_ptr(currentModelMatrix));
    glUniform2fv(_positionLocation, 1, glm::value_ptr(_glyphPositions.at(character)));

    mesh->draw();

    currentModelMatrix = glm::translate(currentModelMatrix, horizontalTranslation);
  }
}

With that working correctly, I whipped up some code in the TooltipSystem to render the tooltip's text and background - now correctly scaled - positioned based on the current mouse position. Because of the amount of string operations going, I decided to add an isDirty flag to the Tooltip component, and only set it to true when the string actually changes.

linguine/src/components/Tooltip.h

#pragma once

#include <string>

namespace linguine {

struct Tooltip {
  std::optional<uint64_t> textEntityId{};
  std::optional<uint64_t> backgroundEntityId{};
  bool isEnabled = false;
  bool isDirty = false;

  void setText(std::string text) {
    if (text != _text) {
      _text = std::move(text);
      isDirty = true;
    }

    isEnabled = true;
  }

  std::string& getText() {
    return _text;
  }

  private:
    std::string _text{};
};

}  // namespace linguine

linguine/src/systems/TooltipSystem.cpp

#include "TooltipSystem.h"

#include "components/Drawable.h"
#include "components/Text.h"
#include "components/Tooltip.h"
#include "components/Transform.h"

namespace linguine {

void TooltipSystem::update(float deltaTime) {
  findEntities<Tooltip>()->each([this](const Entity& entity) {
    auto tooltip = entity.get<Tooltip>();

    if (!tooltip->textEntityId) {
      auto textEntity = createEntity();

      auto transform = textEntity->add<Transform>();
      transform->scale = glm::vec3(10.0f, 10.0f, 0.0f);

      auto text = textEntity->add<Text>();
      text->feature = new TextFeature();
      text->renderable = _renderer.create(std::unique_ptr<TextFeature>(text->feature), UI);
      text.setRemovalListener([text](const Entity e) {
        text->renderable->destroy();
      });

      tooltip->textEntityId = textEntity->getId();
    }

    if (!tooltip->backgroundEntityId) {
      auto backgroundEntity = createEntity();
      backgroundEntity->add<Transform>();

      auto drawable = backgroundEntity->add<Drawable>();
      drawable->feature = new ColoredFeature();
      drawable->feature->meshType = Quad;
      drawable->feature->color = { 0.05f, 0.05f, 0.05f };
      drawable->renderable = _renderer.create(std::unique_ptr<ColoredFeature>(drawable->feature), UI);
      drawable.setRemovalListener([drawable](const Entity e) {
        drawable->renderable->destroy();
      });

      tooltip->backgroundEntityId = backgroundEntity->getId();
    }

    auto textEntity = getEntityById(*tooltip->textEntityId);

    auto text = textEntity->get<Text>();
    text->renderable->setEnabled(tooltip->isEnabled);

    auto backgroundEntity = getEntityById(*tooltip->backgroundEntityId);

    auto drawable = backgroundEntity->get<Drawable>();
    drawable->renderable->setEnabled(tooltip->isEnabled);

    if (tooltip->isEnabled) {
      auto textTransform = textEntity->get<Transform>();
      auto backgroundTransform = backgroundEntity->get<Transform>();

      if (tooltip->isDirty) {
        const auto& s = tooltip->getText();

        auto lineLength = 0;
        auto lineCount = 1;
        auto longestLine = 0;

        for (auto& character : s) {
          if (character == '\n') {
            if (lineLength > longestLine) {
              longestLine = lineLength;
            }

            lineLength = 0;
            ++lineCount;
          } else {
            ++lineLength;
          }
        }

        if (lineLength > longestLine) {
          longestLine = lineLength;
        }

        backgroundTransform->scale = {
            10.0f * static_cast<float>(longestLine) + 10.0f,
            10.0f * static_cast<float>(lineCount) + 10.0f,
            0.0f
        };

        text->feature->text = s;
        tooltip->isDirty = false;
      }

      auto width = static_cast<float>(_renderer.getViewport().getWidth());
      auto height = static_cast<float>(_renderer.getViewport().getHeight());
      auto x = _inputManager.getCursorLocation().x * width - width / 2.0f;
      auto y = _inputManager.getCursorLocation().y * height - height / 2.0f;

      textTransform->position = { x + 15.0f, y + backgroundTransform->scale.y, 0.0f };
      backgroundTransform->position = { x + backgroundTransform->scale.x / 2.0f + 5.0f, y + backgroundTransform->scale.y / 2.0f + 10.0f, 0.0001f };

      tooltip->isEnabled = false;
    }
  });
}

}  // namespace linguine

This system actually works correctly. Any system can query the Tooltip component and call the setText() method on it to show the text for the next frame. I decided to add a tooltip value to each Spell, and update the HudSystem to detect the Hovered state of the AbilityButtons and set the text of the tooltip appropriately.

linguine/src/systems/HudSystem.cpp snippet

findEntities<Tooltip>()->each([this](const Entity& tooltipEntity) {
  auto tooltip = tooltipEntity.get<Tooltip>();

  findEntities<AbilityButton, Hovered>()->each([this, &tooltip](const Entity& abilityButtonEntity) {
    auto abilityButton = abilityButtonEntity.get<AbilityButton>();
    auto abilityEntity = getEntityById(abilityButton->abilityEntityId);
    auto ability = abilityEntity->get<Ability>();

    tooltip->setText(ability->spell.tooltip);
  });
});

Here's the kicker: the way we decided to dynamically add and remove ability buttons has resulted in the buttons rapidly switching places every frame. It's invisible to the player because we also set the correct IDs in all the right places. The problem is that the Hovered state is based on the previous frame! With the buttons switching places all the time, I end up reading an incorrect ID from the correct location from the previous frame.

To fix this, I completely changed how the HudSystem managed party members by introducing a new HudDetails component, which contains the IDs of the health bar and ability button entities for that party member. First, we query all of the current entities with the HudDetails component, and destroy any of the results that don't exist in the player's Party, as well as the associated health bar and ability button. Then we iterate over all the party members to update the details of all the health bars and ability buttons as normal. The result is that each party member consistently owns a consistent set of HUD resources, rather than randomly reassigning ownership each frame based on iteration order.

Ability Tooltips

The cursor doesn't show up in the screenshot, but it's hovering over the green "E" button. You get the point.

Player Decision-Making

We've done a lot of work on the auxiliary systems that will support the very first thing the player has to do: choose their first wisp.

I'm going to place three visual entities in the world, representing the wisps that the player may choose from. As the player approaches each one, they will dynamically be added to the player's party. The player will discover that each wisp is currently injured, and they must use their newly granted ability to heal the wisp. The wisp will be removed from the player's party upon walking away from it. This will also remove the player's access to that wisp's ability.

Upon healing all three wisps with their respective abilities, the player must choose which wisp they want to take with them. I honestly don't know how I'll inform the player that they can only take one - or how I will justify that arbitrary limitation. It can't be any worse than just leaving the third Poké Ball on the table in Professor Oak's lab, right?

I'm going to create a new scene, since this won't really fit the mold of a "boss fight prototype". The new TutorialScene is a copy of the BossFightPrototypeScene with the enemy entity removed. I've also modified the level to be rectangular, though I have no strong reason for that.

The first new element in the scene is a small quad that is representative of a wisp that exists outside of the player's party. I don't want the player to be able to walk over the wisp, so I added a static box collider to it. The intent is obviously for the wisp to be added to the player's party as the player gets near it, so I need a Trigger of some sort. Since the wisp entity already has a collider that is not a trigger, I'll need to create a separate entity for the trigger.

The trigger entity is very simple, containing a circle collider with a radius of 2.0f. In order for this trigger to do anything, we'll have to add behavior for it with a new system. Since this system will be specific to the TutorialScene, I've created a new subdirectory named tutorial/ within the systems/ directory, and named the new system the WispTriggerSystem.

My first pass was to create a new WispTrigger component that contained an internal WispType enum to specify which wisp should be added to the player's party. The system created a new entity, configuring it based on the value within the component, and added it to the party. The behavior was strange because every time the player was in range of the wisp, its state was reset, and you had to heal it again.

Instead, I decided to add the Health and Ability components directly to the Trigger entity, and get rid of the WispTrigger concept altogether. Now when the player enters the trigger range, the already-existing trigger entity's ID is added to the player's party, and when the player is out of range, the party is cleared. When the player repeatedly enters the range of the trigger, the state of the wisp's health and ability cooldown is preserved, which hopefully makes more sense to the player.

linguine/src/systems/tutorial/WispTriggerSystem.cpp

#include "WispTriggerSystem.h"

#include "components/Ability.h"
#include "components/Health.h"
#include "components/Hit.h"
#include "components/Party.h"
#include "components/Player.h"

namespace linguine {

void WispTriggerSystem::fixedUpdate(float fixedDeltaTime) {
  findEntities<Player, Party>()->each([this](const Entity& playerEntity) {
    auto party = playerEntity.get<Party>();
    party->memberIds.clear();

    findEntities<Hit, Ability, Health>()->each([&playerEntity, &party](const Entity& triggerEntity) {
      auto hit = triggerEntity.get<Hit>();

      for (auto entityId : hit->entityIds) {
        if (entityId == playerEntity.getId()) {
          party->memberIds.push_back(triggerEntity.getId());
        }
      }
    });
  });
}

}  // namespace linguine

The first bug I noticed with this system is that the health bar of the wisp was not actually centered, even though mathematically, the HudSystem should be keeping the party's health bars centered on the screen. It turns out that the CollisionSystem was duplicating entity IDs within the Hit component's vector, causing the strange offset. I changed the std::vector in the Hit component to be a std::unordered_set instead, so that there would never be any duplicates.

The next bug had to do with the instant-cast ability's cooldown. If you walked in range of the wisp that had an ability with a cooldown (notably only the instant-cast ability right now), used the ability, immediately walked out of range, and then back in range, its cooldown would not recovery while the player was out of range. Apparently this was because the CooldownProgressSystem was only keeping track of abilities that were currently bound to AbilityButtons. Without the button on the screen, the cooldown just never recovered. The solution was simply to iterate over all Ability components in the scene and properly recover their cooldowns, separately from the AbilityButtons' visual progress.

Finally, the last bug I noticed (though not necessarily the last one I will find), was that the visual indicator for our HoT ability's Effect would not go away when the player left the range of that wisp. This was caused by the EffectSystem assuming that all effects would be associated with a currently-visible health bar. I modified the system to hide the effect's progress bar if the health bar for the entity was not found, rather than just leaving it on the screen.


At this point, the player can walk between the different wisps and heal them using the different abilities that they provide. That's great, but we still need a way to teach the player how to use the abilities. I'm going to add a couple of new features that I hope will help the game feel more intuitive for players that aren't already accustomed to healing mechanics in other games.

First, I'll add a bit of text to inform the player that they can choose an intended target by hovering over a health bar. I'll create two text entities - one that should appear next to the health bar, and another that should appear next to the ability button. When the player first approaches a wisp, I'll show the first text entity, informing them to hover over the health bar. When they successfully hover over the health bar, I'll hide the first text entity, and show the second entity instead, informing them to press the hotkey in order to cast the ability on the target.

In order to dynamically hide and show the text, I'll need a couple of scene-specific components. I'll have a marker component for each blob of text, so that I can query for them specifically - we'll just call them TutorialText1 and TutorialText2. I'll also need a component to track the current state of the tutorial - let's just call it TutorialState.

The TutorialState component contains an internal enum with three "stages": TeachTargeting, TeachCasting, and Completed. After creating the text and tutorial entities in the scene, I modified the WispTriggerSystem to step through the tutorial as the player takes the intended actions, and dynamically display the text based on the tutorial state and the party size.

linguine/src/systems/tutorial/WispTriggerSystem.cpp

#include "WispTriggerSystem.h"

#include "components/Ability.h"
#include "components/Cast.h"
#include "components/Health.h"
#include "components/HealthBar.h"
#include "components/Hit.h"
#include "components/Hovered.h"
#include "components/Party.h"
#include "components/Player.h"
#include "components/Text.h"
#include "components/tutorial/TutorialState.h"
#include "components/tutorial/TutorialText1.h"
#include "components/tutorial/TutorialText2.h"

namespace linguine {

void WispTriggerSystem::fixedUpdate(float fixedDeltaTime) {
  findEntities<TutorialState>()->each([this](const Entity& tutorialStateEntity) {
    auto tutorialState = tutorialStateEntity.get<TutorialState>();

    if (tutorialState->stage == Stage::TeachTargeting) {
      findEntities<HealthBar, Hovered>()->each([&tutorialState](const Entity& healthBarEntity) {
        tutorialState->stage = Stage::TeachCasting;
      });
    }

    if (tutorialState->stage == Stage::TeachCasting) {
      findEntities<Cast>()->each([&tutorialState](const Entity& castEntity) {
        auto cast = castEntity.get<Cast>();

        if (cast->abilityEntityId) {
          tutorialState->stage = Stage::Completed;
        }
      });
    }

    bool isPartyEmpty = true;

    findEntities<Player, Party>()->each([this, &isPartyEmpty](const Entity& playerEntity) {
      auto party = playerEntity.get<Party>();
      party->memberIds.clear();

      findEntities<Hit, Ability, Health>()->each([&playerEntity, &party](const Entity& triggerEntity) {
        auto hit = triggerEntity.get<Hit>();

        for (auto entityId : hit->entityIds) {
          if (entityId == playerEntity.getId()) {
            party->memberIds.push_back(triggerEntity.getId());
          }
        }
      });

      isPartyEmpty = party->memberIds.empty();
    });

    findEntities<TutorialText1, Text>()->each([&tutorialState, &isPartyEmpty](const Entity& tutorialTextEntity) {
      auto text = tutorialTextEntity.get<Text>();
      text->renderable->setEnabled(tutorialState->stage == Stage::TeachTargeting && !isPartyEmpty);
    });

    findEntities<TutorialText2, Text>()->each([&tutorialState, &isPartyEmpty](const Entity& tutorialTextEntity) {
      auto text = tutorialTextEntity.get<Text>();
      text->renderable->setEnabled(tutorialState->stage == Stage::TeachCasting && !isPartyEmpty);
    });
  });
}

}  // namespace linguine

In addition to the tutorial, I'd like to add a simple indicator showing the current target. All I really need is a way to make the currently targeted health bar "pop" out to the user. To do this, I'll just render another quad behind the currently hovered health bar, so that it appears to have a border.

I added a new TargetIndicator marker component, and modified the PlayerControllerSystem to dynamically update the position and visibility of the indicator.

linguine/src/systems/PlayerControllerSystem.cpp update() snippet

findEntities<TargetIndicator, Drawable, Transform>()->each([this](constEntity& entity) {
  auto targetIndicatorDrawable = entity.get<Drawable>();
  targetIndicatorDrawable->renderable->setEnabled(false);

  auto targetIndicatorTransform = entity.get<Transform>();

  findEntities<HealthBar, Hovered, Transform>()->each([&targetIndicatorDrawable, &targetIndicatorTransform](const Entity& healthBarEntity) {
    auto healthBarTransform = healthBarEntity.get<Transform>();
    targetIndicatorTransform->position = healthBarTransform->position;

    targetIndicatorDrawable->renderable->setEnabled(true);
  });
});

A couple of bugs before I move on.

  1. Pesto crashes for seemingly no reason, but very rarely. Alfredo never crashes. This is probably due to some platform-specific nuance.

I spent quite a bit of time trying to figure out how to reproduce the issue with Pesto. It turns out that it crashes when my mouse cursor exits the canvas, but only from the upper edge. After a bit of debugging and logging, I found out that the mousemove callback was being invoked with coordinate values outside of my canvas (sometimes with bogus values like the max value of a 64-bit integer). I added some bounds checks against the viewport dimensions to the event handler so that it would never make it to the rest of the engine, but it was still crashing.

After more debugging, I realized my mistake. I flip the Y-axis by subtracting the normalized Y coordinate from 1.0f, so if the normalized Y value was 0.0f, then the result is 1.0f. However, 1.0f is not a valid location to sample from the "selectable" texture. Since the texture must be queried by pixel locations between 0 and height - 1, multiplying the viewport height by 1.0f returns height, which is greater than height - 1. The solution is to add 1 to the Y coordinate before normalizing and flipping it.

  1. The tutorial text fights for visibility against the ability tooltips.

This one is much easier. I just needed to set the Z coordinates of the text entities to 1.0f so that the tooltip would always appear in front.


Now then, how do I go about making the player actually choose one of these wisps? I mean, I know that I could just display a button on the screen asking the player if they want to keep their current wisp - but why? Why are they here? Why must they choose? Why can they only choose one?

Video games are notorious for their bad narratives. There are some exceptions, of course, but overall video games must prioritize gameplay over storytelling, or else no one would bother playing them. Some games - usually RPGs - make attempts to tell compelling stories while providing enjoyable gameplay experiences for the player. Others provide almost no detail beyond simple reasons for the player to continue playing.

Most Mario games are simply about overcoming platforming challenges in an attempt to rescue the princess - there's really not much more to it than that. Why are these enemies getting in your way? What does collecting coins have to do with saving the princess? Why did a flower give you the ability to shoot fireballs? Absolutely none of it makes any sense, and yet, it's widely considered to be one of the most enjoyable gameplay experiences of all time.

Overcooked! has a similarly absurd storyline: there's a giant spaghetti monster taking over the world, and you must go back in time to learn how to be a better chef in order to defeat the spaghetti monster. What?!

This is ultimately the reason I've been so focused on the gameplay aspect of the game so far, without much consideration for storytelling. A fun game is enjoyable, regardless of how bad the story is. A boring game with a good story won't have their players stick around long enough for them to even experience the story.

World of Warcraft is an interesting counter-example. While they do put extraordinary effort into the core gameplay and class design, the actual raid and dungeon encounters are built around the story. If the story being told has a lot of dragons, then naturally the fight mechanics will generally include a lot of fire breathing and massive claw swipes.

I really just have no clue what direction to go, and I've already achieved what I really set out to do in this chapter: implement the core mechanics of the game in an extensible fashion. With that done, I think I'll wrap up this chapter and take some time to think about what type of storyline this game might have. I've also had the urge to research different possibilities of visual styles, which is even more intimidating to me than storytelling. Ultimately, I need to figure out what the game is, now that I've established the core mechanics.

The latest code can be found here, and the latest playable version of the game can be found here. There's certainly a lot of room for improvement from a "gameplay" perspective, but at least it proves that the underlying systems are working as intended.