Project Delve Part 4.3: Various Enhancements

So for this post, unlike the posts I’ve made before, I’m not going to be walking you through step-by-step how I created one thing or another (though I will explain the general concept). This is because I’d like to move on to Part 5 in the series posts but there were some things I felt needed to be addressed first, both incomplete things and quality of life things. There were quite a few changes I made that would take a lot of time explaining each one in detail when they felt more or less unrelated to the overall structure of the project. So with that said, let’s jump in!


Pathfinding Misunderstanding

One thing I do want to explain fully, though, is a “bug” I thought I had found while testing out the pathfinding script. (See the issue I posted on the Godot Github repo here.) In short, I thought that the AStar.get_x_path() functions were incorrectly calculating the overall weight of the path, and would not always return the path with the shortest weight.

Because Godot is open-source, I was able to look through the engine’s source code and found the implementation of their pathfinding algorithm. I realized I had incorrectly assumed the algorithm was based on a grid-like structure, but when I looked at the source code I saw that the distance between points was calculated based on Euclidean distance, and not by grid-tile neighbors.

This is important because, if you remember from your geometry class, the Pythagorean Theorem states that the third side of a right triangle is equal to the square root of the sum of the square of the other two sides. Or, in other words, a2 + b2 = c2.

So while the distance between two neighboring orthogonal points on our grid is 1, the distance between to neighboring diagonal points on the grid is 1.41. Because of this false assumption I was working under that the distance between any neighboring points would be 1 * <weight of the point>, there was a problem when the diagonal point had a weight greater than 1 (e.g. a water tile).

A gif of the issue

Of course this makes sense when you think about all the various applications of AStar that needed to be accounted for, but I wasn’t sure how to solve the issue. Fortunately a helpful person answered my issue with the solution: extend the AStar class and override the _compute_cost() function. After doing that the pathfinding worked as expected!

# pathfinder.gd

onready var _a_star = GridAStar.new()

class GridAStar:
    extends AStar2D

    func _compute_cost(from_id, to_id):
        var pointA = get_point_position(from_id)
        var pointB = get_point_position(to_id)
        return floor(pointA.distance_to(pointB))

Caching Paths

Also having to do with the pathfinder, I made an optimization on the CharacterController class that caches all calculated paths, based on the destination point, until the character moves. This is going to be helpful because we won’t need to ask the Pathfinder to repeatedly calculate the path to the same spot from the same spot if we’ve already done it before. Additionally I’m also caching the cost associated with each calculated path for the same reason. I made a simple Dictionary object in the CharacterController for these caches, and a simple _key() function for the cache key.

func _key(loc: Vector2, pathfinder: Pathfinder) -> String:
    return str(pathfinder.convert_to_map_point(loc))

Stamina

I factored in stamina to a unit’s movement. A unit is able to move up to its speed without using stamina, and then one stamina for each additional movement point. A unit only has a finite amount of stamina which they can only recover through resting or other special actions.

showing stamina-based movement

You may also notice the yellow/orange bars underneath the unit’s health bar. I thought it would be a good way to represent the remaining stamina points a unit has. The fun part about making the stamina bar was that I did it through a shader script! I do plan on devoting a full post to the usage of shaders sometime in the future, but the short explanation of what a shader is, for those who may be unfamiliar (like I was before doing this!), is that a shader is able to manipulate pixels in a texture in various ways.

There’s a really helpful and interesting video on the topic of shaders in Godot on YouTube that I think covers the topic very well for beginners like me. You can find that video here.


Path Highlighting

Something that I find very helpful in tactical games is the indication of a route a unit will take when they move. I wanted something like that in this game, so I made it! Currently it’s very rudimentary and unpolished, but it gets the job done.

Godot makes it very easy to draw on the screen through code. (See the docs for more info.) All I needed to do was calculate the path to the final destination through the Pathfinder and then draw a line segment from point to point until reaching the end. Simple! The tricky part was displaying an arrow to cap the line when it reached the destination. I made a simple arrow head image using the online pixel art tool Pixil, then imported it into the project. I made it white so that I could change its color to be whatever I wanted in the code, and I also made the image size much bigger than 16×16 so that I could scale it down and not have it be so hard-edged on the diagonal parts.

I added a new node onto the Dungeon scene called PathDrawer and attached a new script to it by the same name, then surfaced two public functions: draw_path() and erase_path(), one that sets the internal _path array and one that clears it. Then I overrode the _draw() function to draw a line from point to point like I explained above, but to draw the arrow I needed to do some transformations to the image before drawing it.

Because the default origin point of an image is the top left corner, I needed to offset the image by the full height of the texture and half of the width (since I drew the arrow from the bottom middle pointing up). I also needed to rotate it based on the direction of the final two line points and scale it by the appropriate factor so that it would be sized correctly relative to the line width and tiles. Ultimately that code came out looking like this:

var scale = 8.0
var scale_size = Vector2(1.0 / scale, 1.0 / scale)
var arrow_size = arrow_point.get_size()

var final_loc = _path[-1]

var offset = last_dir * arrow_size
offset += last_dir.rotated(-PI / 2.0) * arrow_size / 2.0
offset -= last_dir * arrow_size / 2.0

draw_set_transform(final_loc + offset * scale_size, last_dir.angle() + PI / 2.0, scale_size)

draw_texture(arrow_point, Vector2.ZERO, path_color)

And this was the end result. Much more clarity to movement!


Camera

Another thing that has been bugging me for a while is how zoomed out the scene seemed when I would play it. (You can’t tell from the gifs I post because I crop them, but the grid size was very small compared to the window size.)

grid size compared to window size

I definitely don’t want that to be the size of the screen when this is all playable! So I created a Camera2D node as a child to the Dungeon scene. Unfortunately the out-of-the-box camera node for Godot doesn’t come with a lot of helper functions that the camera in Unity comes with, so I had to implement the one I needed myself.

When using a camera, you have to do a few calculations to get the point in world-space relative to the point on your screen (screen-space). Unless the camera is positioned at the origin of the world, clicking somewhere on your screen won’t translate to its point in the world.

After some trial and error and reading up on transformation matrices, I realized all I needed was a simple calculation to convert the point in screen-space to the correct point in world-space. Fortunately we’re working with a 2D orthogonal camera, which makes the transformation much simpler. I wrote a script that extends Camera2D and added the calculation function to it. In the end, this was the whole script.

extends Camera2D

class_name DungeonCamera

func screen_to_world_point(point: Vector2) -> Vector2:
    return point * zoom + position

Now whenever I need to get the world-coordinates of a point on the screen I can just run that point through this function and be on my merry way!


Unit Selection Indication

The final thing I implemented was a way to tell if you were hovering over a unit, or if they were being targeted in some way. For this I felt like showing an outline around the character sprite would be a good solution, and figured the best way to do this would be to use another shader. However, I wasn’t sure where to start in creating a shader to do that so I did a quick search on the ol’ Googs and found this shader that did exactly what I needed! So giving credit where credit is due, thank you Juulpower for your contribution to a good cause.

After applying this shader to unit sprites I made a few modifications to it so that I could enable and disable it at will, as well as make it flash for effect. It added the clarity I felt the game needed when indicating selection!


Results

Putting all of these things together, I feel like the state of our game is much more clear on what is happening! It’s still not perfect of course, but it’s progress in the right direction!

Here’s a quick render of the state of the game as it currently stands.

gif of the results put together

Because I didn’t give a detailed rundown of all the individual changes I made, I recommend taking a look at the code up to this point. That can be found here. I’ll be back soon with the next part of the project: determining line of sight!

Leave a Reply

Up ↑

Discover more from The Walrus Game Project

Subscribe now to keep reading and get access to the full archive.

Continue reading