5 min read

Why not? - Make your own Fire Emblem movement system in Godot!

Why not? - Make your own Fire Emblem movement system in Godot!
šŸ„‡
This is a Conceptual Introduction and not a Step by Step Guide. Let this spark your imagination, experiment and have fun!

I have been gallivanting around Europe the last couple of months, and have been out of the saddle when it comes to working on Knott and the Waking Wilds. I decided to create a little Demo project to get myself back into the Game Development Space.

Recently I have been playing around with Custom resources, with these I have been able to add property sheets to nodes such as player stats and weapon details. With it, I seem to have created a pretty decent solution for a Advanced Wars/Fire Emblem board type system.

First, I made a custom MissionMap node which extends TileMapLayer, As well as a dictionary with all the current units. The MissionMap’s has two goals, Hold the Unit Database and supply tile info. These are marked with a Custom Layer (Open, Mountain, Blocked Ect.) You can pull these properties with the following line of code and set up your own TileSet in Figure 1.

get_cell_tile_data( *VECTOR2I* ).get_custom_data( *CUSTOM_LAYER_NAME* )
Figure 1 - Tile Property Setup

Next, I created a new resource called a MapQuery. This allows me to make map queries and refer to them the same way you would create a Vector2 or similar. MapQuery.new(UNIT,MAP), The movement ranges are saved in a dictionary.

The code works as follows,

  1. First it gets the unit location. Using the unit location. It sets the dictionary record as followed, Key is the Vector2i where the unit is, the value is set to the max_move of the character.
  2. Then the following is looped to adjacent spaces with the movement weight being reduced by one until no more spaces can be done.
    1. Is movement equal to or less than zero. If so cancel check.
    2. Is the tile requested out of bounds, If so cancel check.
    3. Is there an existing record for this tile in the movement_range dictionary, If so does that space have a higher movement weight? If the movement is larger cancel check. (No need to check the tile as its neighbors have already been checked.)
    4. Check the MissionMap Unit Database to see if a unit is on that tile, If there is a unit, If it is from a different team, cancel the check.
    5. Check the weight of the tile being checked, If ā€œblockedā€ (Solid Wall) cancel check. (Data from Figure 1 screenshot)
    6. If the remaining weight is less than -1. Cancel check.
    7. Finally, This is a valid tile! Add this to the movement_range dictionary.
    8. Then schedule the tile at the North South East and West to be checked. Eventually every possible tile will be correct,
šŸ’”
Note: Whenever the code lands on return, It is not adding the Tile to the ā€œmovement_rangeā€ dictionary.
func query_move(coordinates,movement):
	#Cancel if no movement remains
	if movement < 1:
		return
	
	#Cancel if tile is Out of Bounds
	if map.is_vector_OOB(coordinates):
		return
	
	#Check if an existing record has a higher movement record.
	if movement_range.has(coordinates):
		if movement_range[coordinates] >= movement:
			return
	
	#Check if an enemy unit is on the space cancel if so.
	var occupying_unit = map.get_unit_by_vector(coordinates)
	
	if occupying_unit != null:
		if occupying_unit.team != unit.team:
			return
	
	var weight = 1
	#Assign movement costs for special tile types.
	match map.get_cell_tile_data(coordinates).get_custom_data("TileProperty"):
		"Blocked":
			return
		"Water":
			weight = 4
		"Mountain":
			weight = 2
	
	#Cancel if not enough movement remains
	if movement - weight < -1:
		return
	
	movement_range[coordinates] = movement 
	
	schedule_next(coordinates,movement - weight)
		
	
func schedule_next(coordinates,movement):
	if coordinates != initial_space:
		check_neighbours(coordinates,movement)

func check_neighbours(coordinates,movement):
	query_move(coordinates + Vector2i.UP    , movement) ##ABOVE TILE
	query_move(coordinates + Vector2i.RIGHT , movement) ##RIGHT TILE
	query_move(coordinates + Vector2i.DOWN  , movement) ##BELOW TILE
	query_move(coordinates + Vector2i.LEFT  , movement) ##LEFT TILE

Now that we have the players squares that can move too, we can query the movement_range database for the attack range. For each movement square, we will do the same the same thing as the movement layer and check checking the weight of each attack tile. However we do not need the weight of terrain or if there is a unit blocking the attack.

func calculate_attack_range():
	for coordinates in movement_range:
		query_attack(coordinates,unit.properties.Attack_Range)

How query_attack() works.

  1. If it is out of range, Cancel check.
  2. If it is out of bounds, Cancel check.
  3. If attack_range already has a record, If it is larger than the current range. Cancel Check.
  4. Finally this is a valid tile add to the attack_range dictionary.
  5. Continue to check neighbours.
func query_attack(coordinates,range):
	if range < 0:
		return
	
	if map.is_vector_OOB(coordinates):
		return
	
	if attack_range.has(coordinates):
		if attack_range[coordinates] > range:
			return
	
	attack_range[coordinates] = range
	
	query_attack(coordinates + Vector2i.UP    , range - 1)
	query_attack(coordinates + Vector2i.RIGHT , range - 1)
	query_attack(coordinates + Vector2i.DOWN  , range - 1)
	query_attack(coordinates + Vector2i.LEFT  , range - 1)

In the end you should have two ranges with the following distances. For each tile in the dictionary I paint the tiles on a separate TileMapLayer. See Figure 2 for what the weight distribution would be for a unit that has 3 move and 2 attack range.

Figure 2 - Weight Example

The next steps for this demo will be to query the attack range and deal damage to the enemy. Looking forward to play around with this demo again.

If you would like to support my games,, Please check out my site with games on the the Apple App Store or the Google Play Store!

buddibeacon.com - Small Indie Games with Bigger Heart!
buddibeacon.com

Thanks for your time!
Nathaniel