Basic player user interface
Now that we have the player movement with action implemented, it would make sense to display this information to the player, so they can see what’s going on with their Action resource when they perform their actions.
I made a UI (User Interface) scene
PlayerUI, which included two
ProgressBar subnodes and two
Label subnodes, one for Health and one for Action.
For the two
ProgressBars, I needed hide the percentage display and set the
Custom Style to be a
StyleBoxFlat for both the foreground and the background, with the appropriate colors, to emulate the look-and-feel for a Health and an Action bar.
Now, to add it to our
World scene’s UI, while making it appear over the top of our camera, the
PlayerUI instance needed to be added as a child of a
CanvasLayer subnode to the
However, one of the most important parts of making this UI is connecting it with the player’s actual Health and Action information. To do so, we add an
export reference to the player to the script of the
PlayerUI, and use the information provided by the fields of the
Player to set the values of the process bars (we added a
health field to the
Player with default value 100):
export (NodePath) var player const full_health = 100.0 const full_action = 5 func _process(delta): if player: var node = get_node(player) $HealthBar.value = node.health * 100 / full_health $ActionBar.value = node.action * 100 / full_action
Now, we get a UI overlay of the player’s Health and Action, and we can see the live-updating of the Action depletion of the player.
In perparation to adding enemies, a refactor of the
Player script was made to make pull out the functionality that was common for both the player and enemies into the
Entity script. A few functions that are the equivalent of “abstract methods” were added to the
Player script looks something like this (the rest of the functionality is in
extends "res://Entity.gd" const move_cost = 0.5 const action_depletion = 1.0 func _get_action_depletion(): return action_depletion func _get_move_cost(): return move_cost
Unfortunately, GDScript does not support mixins at the current moment, based on a Github issue referencing the missing feature. Mixins are essentially pieces of functionality that can be “mixed in” with objects. This can be used in place of inheritance that is typically associated with Object-oriented programming. One major advantage of mixins is that a single object can have multiple mixins, removing the need of either a single
Entity class containing most of the functionality needed in many types of entities, or code being duplicated for every single entity. Instead, mixins such as
PathfindingMixin can be added as needed by specific entity types. Mixins are especially suited for dynamically typed languages, where the functions and properties of an object can change at runtime, and mixins can be added or removed at will.
A bit of a sidetrack in our effort to implement enemy behavior - moving characters instantaneously between spaces is not very interesting, and makes for some quite jarring gameplay. Instead, some curve-fitting can be used as a transition for a player in between moving from one position to another. This is especially fitting for this game, because of the action delay on movement, so the game knows for how long the movement needs to last.
The most obvious type of smoothing is known as linear interpolation, or
lerp. This is a very popular method, as it is a very simple way of choosing a point between
end. Simply take the
weight, which is a number between 0 and 1, where 0 represents fully at the
start and 1 represents fully at the
end, and as the name would suggest, linearly interpolate. In other words, if
weight is 0.25,
lerp puts the point one quarter of the way from the start to the end.
However, simply lerping can still look somewhat unnatural for movement. Instead, we can use the
smoothstep function (from the mathematical Smoothstep function) provided by the Godot engine for our curve-fitting function.
smoothstep is similar to
lerp in that it chooses exactly the start point when the input is 0 and the end point when the input is 1, but the difference is that the derivative of
smoothstep is zero at both end points, making the start and end of the motion more “smooth”.
The animating code is fairly complicated compared to the rest of the code that I had written so far. Essentially, what was happening was that we needed to keep track of the start position of the animation, the end position of the animation, and the progress of the animation, in a way that persisted across frames. The following
_animate_move function is called by
_process each frame, and the
_start_move_animation function is called by the
move function when a move is performed, to set the appropriate values (
_get_travel_time is an abstract function of
Entity that is implemented in
#Move animation variables var animateStartX = 0 var animateStartY = 0 var animateEndX = 0 var animateEndY = 0 var animateTotalTime = 0 var animateTimeLeft = 0 func _animate_move(delta): if animateTimeLeft < 0: animateTimeLeft = 0 displayX = x displayY = y if animateTimeLeft > 0: var weight = 1 - animateTimeLeft / animateTotalTime var step = smoothstep(0, 1, weight) var resX = step * (animateEndX - animateStartX) + animateStartX var resY = step * (animateEndY - animateStartY) + animateStartY displayX = resX displayY = resY animateTimeLeft -= delta func _start_move_animation(x, y, dx, dy): animateStartX = x animateStartY = y animateEndX = x + dx animateEndY = y + dy animateTotalTime = _get_travel_time() animateTimeLeft = _get_travel_time()
Although it’s difficult to precisely describe the updated smoothstep movement in words, as the effect is much more pronounced in the actual gameplay, here is a screenshot of the player moving in between two locations on the grid:
In the next step in developing this game, we’ll finally get to add enemies and skills, for a minimum viable product. After that, there are many further directions that can be taken. Stay tuned!