Just recently I added HorizontalFlow
component to Warp, which allows you to render child components horizontally:
As you can see above, I have three buttons, which are all part of the same container. Instead of positioning them manually, I used a dedicated container, which takes of positioning automatically. Conceptually, HorizontalFlow
works in the same way as VerticalFlow
. The only difference is logic responsible for repositioning children it contains.
Once I added buttons, I wanted to implement one more thing - once a player clicks on a button, the button must stay in hovered state. Let’s check the current implementation.
isActive
propertyUI components should be allowed to indicate, that they’re active. However, being active may have different meaning for different components. This is a problem - under normal circumstancies I’d say, that each component is supposed to override setter of isActive
property. As Warp is written in vanilla JS, which has rather limited OOP capabilities (meaning - you can achieve almost everything, but some solutions are clumsy as hell), I started thinking about something more robust. This is when an idea of __notifyPropChanged
was born.
Warp’s base class GameObject
introduced a boilerplate __notifyPropChanged
method acting as a default implementation so we can avoid errors:
/**
* @description Allow to register a hook to be called when a property changes.
* @date 12/31/2023 - 2:16:14 PM
*
* @param {*} property
* @param {*} oldValue
* @param {*} newValue
*/
__notifyPropChanged(property, oldValue, newValue) {
return;
}
This immediately enables us to use that method in some real case scenarios. For instance, Warp currently has a convention, which introduces publishing mechanism in each setter defined in any of available classes. Here’s example of setters for x
and y
properties, which are defined inside GameObject
:
set x(value) {
this.__notifyPropChanged('x', this.#x, value);
this.#x = value;
this.markDirty();
}
set y(value) {
this.__notifyPropChanged('y', this.#y, value);
this.#y = value;
this.markDirty();
}
The same convention is used by UIObject
, where isActive
property is defined:
set isActive(value) {
this.__notifyPropChanged('isActive', this.#isActive, value);
this.#isActive = value;
this.markDirty();
}
To wrap up implementation, I added __notifyPropChanged
override in Button
class:
__notifyPropChanged(property, oldValue, newValue) {
super.__notifyPropChanged(property, oldValue, newValue);
if (property === 'isActive' && newValue === false) {
this.fillColor = this.__fillColor;
}
}
This allows us to reset button’s color each time it’s set as not active. Let’s see now how one can use those new concepts when describing a scene.
An example of a button component definition could look like this:
const newGameMapSizeSmallButton = new Button('main-menu-new-game-map-size-small-button', {
x: 0,
y: 0,
width: 50,
height: 30,
anchor: UI_OBJECT_ANCHOR_MIDDLE_CENTER,
parent: newGameMapSizeSelectionHorizontalFlow,
fillColor: COLOR_PRIMARY_BACKGROUND,
borderColor: COLOR_OUTLINE,
text: 'Small',
textColor: COLOR_OUTLINE,
fillColorHighlight: COLOR_HIGHLIGHT,
eventHandlers: {
onClick: (component) => {
component.isActive = true;
newGameMapSizeMediumButton.isActive = false;
newGameMapSizeLargeButton.isActive = false;
}
}
});
As you can see, there’s onClick()
event handler, which performs three actions:
The result will be as follows:
There’s still one thing missing here (resetting a state of a button once a player clicks outside of buttons inside HorizontalFlow
), but that a matter of adding one more event handler to the root container of UI.