Now that Skymagi has lighting and fog of war, the scrying orb can exist!
The scrying orb (or divination sigil) has a few intended power levels based on how much mana is allocated.
- No mana, deactivated.
- Reveals allied castle interior (under normal weather conditions).
- Reveals enemy castle interior.
- Reveals enemy mage spellcasts.
As for implementation, I created scrylights that illuminate castle rooms. I thought about pairing it with torches hanging on walls, so I gave it an orange hue.
Note the light cascading onto the wall texture thanks to the blocksLight
collision filter.
Sigil Mana Pool
A castle has a set amount of mana (e.g. 10) that it can distribute throughout various sigils. The mana infused into a sigil determines its power level.
As sigils are upgraded, they require more mana. When sigils are harmed, mana nodes are locked and prevent the sigil from reaching its full power.
A rant about Game UIs
Though I’ve long had the UI designed for the sigil mana pool, I had not implemented the actual pool.
I have not used scene2d for the HUD in Skymagi, primarily because it is object oriented and most of the HUD needs to be driven by ECS.
To illustrate what I mean, let’s use some pseudocode similar to scene2d.
// create a new button object
Button button = new Button("My sigil", 0, 0, 50, 50);
// later, in the game loop, render it
button.render();
This seems simple on the surface, but say that this button needed to be linked to an enemy entity’s health.
Entity enemy = new Entity().add(new Health(50)); // 50 hp
// We want the button to show the enemy's hp, so we set the label to match
Button button new Button(enemy.hp, 0, 0, 50, 50);
// but then the enemy gets hurt...
enemy.hp -= 10;
// but our label is still 50, so we need to update it.
button.label = enemy.hp;
button.render();
We could solve this problem through event passing (update label
when enemy is hurt), but that is code heavy.
We could add a persistent pointer so the button always references enemy.hp
, but then we must maintain
that pointer through enemy’s lifecycle.
So, in the end, you have some code like…
Map<Id, Button> buttons = new Map<>();
engine.addEntityListener(
Family.all(Health.class).get(),
// a new enemy is created, add the button
(Entity enemy) -> buttons.put(enemy.id, new Button(enemy.hp)),
// enemy is dead, remove the button
(Entity enemy) -> buttons.remove(enemy.id)
);
public void render() {
// update all the labels (unless we used data binding*)
for (Entity enemy : enemies) {
buttons.get(enemy.id).label = enemy.hp;
}
// render all the buttons
for (Button button : buttons.entries()) {
button.render();
}
}
These UI hierarchies have a lot of pros, such as space adjustment and responsive sizing, but they don’t pair well with a HUD that is constantly reacting to game updates.
I would rather just write an immediate mode UI.
Array<Entity> enemies = Family.all(Health.class).get();
public void render() {
for (Entity enemy : enemies) {
batch.draw(BtnTexture, 0, 0, 50, 50);
font.draw(enemy.hp, 0, 0);
}
}
There is no debugging, I do not have to mutate objects based on status. I use exactly the state per frame. It’s simple and declarative.
So, my actual UiRenderSystem
holds a group of ECS families and draws healthbars, portraits, spellbooks,
cooldowns, etc. No data binding required.
However, that also means event handling isn’t handled for me.
Rather than refactoring the HUD to use Scene2d, I doubled down. I created an ImHud
utility class that
can handle mouse interactions such as hovers and clicks (inspired by ImGui).
private void renderSigil(Entity sigil, float x, float y) {
// defines a clickable region for a particular sigil
imHud.ButtonRect(sigil.id, x, y, SIGIL_SIZE, SIGIL_SIZE);
// immediately calculates if this particular region is hovered
if (imHud.isHovered(sigil.id)) {
batch.draw( ... ); // sigil outline glow
}
// determines, for a single frame only, if the sigil was clicked (and caches)
if (imHud.isClicked(sigil.id, Input.Buttons.LEFT)) {
// All user interactions are done through command components, e.g. changing sigil mana
engine.addEntity(
new Entity().add(new SigilManaCommand(sigil.id, 1))
);
}
// does the actual drawing of the button every frame
batch.draw( ... ); // the sigil texture
}
My background is with web development, so I miss the expressiveness of React/CSS/HTML whenever I do game UIs. I’ve scoured for good solutions, but there is never a simple solution for either running DOM, react native, or JS/TS in addition to a game. It’s just too heavy.
The best part of React is that it is declarative. You do not specify how to mutate your components, you simply return the new state and React makes the necessary changes.
Immediate mode UIs provide a similar experience, but without a shadow DOM that ensures mutations are performed efficiently. For me, the cost is absolutely worthwhile.
I did the work to integrate ImGui into Skymagi, but the performance hit was notable. I kept it
around for debugging, but just use my ImHud
utility.
It worked pretty well.
The UI doesn’t handle any validation, it just sends commands. My SigilManaSystem
processes
those commands and applies the actual changes to the sigil. It also handles validation (you can
see invalid click commands at boundaries in the gif above).
Now the scrying orb
I created a ScrySigilSystem
that enabled or disabled scry lights on a castle based on the sigil’s
current level. The scry lights already had a Faction
component, so I could filter for enemy
or ally.
I chose to have the SigilManaSystem
apply the changes in mana so that each sigil could abstract
their features (e.g. showing allies, enemies) away from mana in case I want to rebalance or add unusual
upgrades.
private void updateSigilLevel(Sigil sigil) {
switch (sigil.sigilType) {
case Scrying -> {
sigil.setLevel(
switch (sigil.manaNodes) {
default -> ScryLevel.NONE;
case 1 -> ScryLevel.ALLIES;
case 2 -> ScryLevel.ALLIES_ENEMIES;
}
);
}
}
}
All together, I can toggle the scry level (0-2) and show everything working in consortium.