This is a part of a series on developing Gridworld Plus. See part 1 or the previous part for more details.

Basic player user interface

A view of our player in the world
A view of our player in the world

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 Control node’s 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.

The health bar and the action bar
The health bar and the 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 World node.

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.

The player and the UI, displayed while the action of the player is cooling down
The player and the UI, displayed while the action of the player is cooling down

Entity script

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 Entity script.

Now the Player script looks something like this (the rest of the functionality is in Entity):

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 PositionMixin, HealthMixin, SkillUserMixin, 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.

Animating movement

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 start and 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 Player).

#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:

The player mid-movement, in between two tiles
The player mid-movement, in between two tiles

Next steps

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!