GameDev Diary: Learning new design structures and Reworking Support Mechanics

Andrew Lukes
5 min readSep 16, 2023

--

More Upgrades and Improved Code Architecture

Glad to see you back in the 9th entry into my gamedev diary. Today I will be talking about a new coding concept I have learned, and how I have implemented it into my strategy game as well as talking about some new features.

Let's go!

Photo by Joshua Reddekopp on Unsplash

The shortcomings of inheritance
What is composition
· How I Implemented It
Support Component
Ranged Attack Component
Melee Component
· Other Features
Improved Buy Bar
Houses
Money System
· Conclusion
DEMO
Previous diary entry:

The shortcomings of inheritance

The standard way to write OOP* classes is to create a parent class (ex. unit) with methods like move, attack, and buy_unit Then you would add children that inherited those methods like Pikeman or Canon, and expand on them in their own unique way. But let's say, that you would have a class like medic, who shouldn't be really attacking enemy units. In that case, you would have to work around it with some weird overrides or separate battle unit and support unit classes and that is where composition comes into place

*Object Oriented Programming

What is composition

Instead of creating classes like units or terrain objects, you would create minicomponents like attack, move, or find enemies. Now every time you create a class (for example pikeman) you add the attack, move, and find the enemy component. But when it comes to the medic, you would only add it to the move component and create a new component called support_comp, which would in the case of my game find the supported units hp value and add 1 extra point to it.

Here is the video that first taught me this technique

How I Implemented It

Support Component

This component is given to all units, that can do a supporting action like healing or resupplying. In each individual instance of this function, I am then defining what variable gets buffed and whether the buff gets added to the supported unit every turn or only once. On top of that the support component creates a line from the parent to the supported unit visually signifying what unit is being supported.

extends Node2D
class_name SupportAction
var supported_entity
var buffed_variable = "attack_range"
var increase_ammount = 200
var constant_buff = true
var buff_already_applied = false
var color = Color(1, 0.75, 0.8)#pink


func deselect_supported_entity():
if supported_entity and constant_buff and buff_already_applied:
if buffed_variable in supported_entity :
supported_entity.buffed_variable -= increase_ammount
supported_entity = null
buff_already_applied = false

func check_can_support():
# series of check for wheter component can support this unit
func choose_supported():
if not check_can_support():
# deselect_supported_entity()
return

supported_entity = Globals.hovered_unit
print(supported_entity, "supported entity")
return "SUCCESS"

func provide_buffs():
if buff_already_applied and constant_buff:
return
print("SUPPORTING ", buffed_variable ,increase_ammount)
if supported_entity:
var entity_to_buff = supported_entity if buffed_variable in supported_entity else Utils.find_child_with_variable(supported_entity, buffed_variable)
print("ENTITY", entity_to_buff, buffed_variable)
if entity_to_buff and entity_to_buff.get(buffed_variable) != null:
print(entity_to_buff.get(buffed_variable))
entity_to_buff.set(buffed_variable, entity_to_buff.get(buffed_variable) + increase_ammount)
buff_already_applied = true
else:
buff_already_applied = false


func draw_line_to_supported_entity():
# draws a colored line to the entity it supports
func _process(delta):
draw_line_to_supported_entity()

Ranged Attack Component

This component is capable of spawning new bullet instances and keeping track of the unit's ammo. Later I plan to add children to this component for handling different types of projectiles.

extends Node2D
signal bullet_shot
signal ammo_changed(ammo)
var bullet_scene:PackedScene = preload("res://scenes/screens/levels/projectiles/bullet.tscn")
var max_ammo: int
var ammo:int = max_ammo:
get:
return ammo
set(value):
ammo = min(value, max_ammo)
ammo_changed.emit(ammo)
var attack_range

func _ready():
connect("bullet_shot", _on_bullet_shot)

func attack(center):
print("called", center, Globals.hovered_unit , Globals.action_taking_unit == self.get_parent() , ammo > 0)
if Globals.hovered_unit and Globals.action_taking_unit == self.get_parent() and ammo > 0:
var direction = (Globals.hovered_unit.global_position - global_position).normalized()
var global_center = center
bullet_shot.emit(global_center, direction)
ammo-=1

func _on_bullet_shot(pos, direction):
var bullet = bullet_scene.instantiate() as Area2D
# Set the position and direction of the bullet
bullet.position = pos
bullet.direction = direction
print("DIRECTION", direction)
bullet.color = get_parent().color
bullet.attack_range = attack_range
# Make the bullet face its direction
var target_pos = pos + direction * 100
bullet.look_at(target_pos)

# Add the bullet to the projectiles node
var projectiles = get_tree().get_root().get_node("BattleGround").get_node("Projectiles")
projectiles.add_child(bullet)

Melee Component

This component was much easier to implement because it only checks whether the enemy can be attacked, and then plays a slashing animation on top of the enemy

extends Node2D

signal attack_failed
signal attack_suceeded
func attack(remain_actions, attack_range, color, attacked_entity):

if Globals.action_taking_unit != self or not Globals.hovered_unit or Globals.hovered_unit.color == color:
attack_failed.emit()
return "failed"
var distance = self.global_position.distance_to(Globals.hovered_unit.global_position)
if distance > attack_range:
attack_failed.emit()
return "failed"
attacked_entity.get_node("HealthComponent").hit(1)
remain_actions -= 1
play_attack_animation(attacked_entity)
return "success"

# toggle_attack_screen()
func play_attack_animation(attacked_entity):
$SlashAnimation.z_index = 1000
# slash_animation.position = Globals.hovered_unit.position #.ZERO # Center of the unit
var collision_shape = attacked_entity.get_node("CollisionArea/CollisionShape2D") # Replace with your actual node path
var shape_size = collision_shape.shape.extents * 2 # For RectangleShape2D and CapsuleShape2D
$SlashAnimation.global_position = Globals.hovered_unit.global_position + shape_size / 2
# Calculate the angle to face Globals.hovered_unit
if Globals.hovered_unit:
var dir_to_hovered = (Globals.hovered_unit.position - position).normalized()
var angle_to_hovered = dir_to_hovered.angle()
$SlashAnimation.rotation = angle_to_hovered

$SlashAnimation.play("slash")

Other Features

Improved Buy Bar

The most important change was, that every button actually spawns the unit it was supposed to. Before it only spawned the default class from which all the units inherit code.

I also found out that I can alter the look of my buttons using Godot themes, so now the black PNGs are actually visible on a lighter background.

buy bar currently
extends HBoxContainer

var unit_types = ["cannon", "pikeman", "shield", "medic", "knight", "musketeer"]
var unit_images = [preload("res://img/cannon.png"), preload("res://img/pikeman.png"), preload("res://img/shield.png"), preload("res://img/medic.png"), preload("res://img/knight.png"), preload("res://img/musketeer.png")]
var unit_scenes = [
preload("res://scenes/screens/levels/units/canon.tscn"),
preload("res://scenes/screens/levels/units/pikeman.tscn"),
preload("res://scenes/screens/levels/units/shield.tscn"),
preload("res://scenes/screens/levels/units/medic.tscn"),
preload("res://scenes/screens/levels/units/knight.tscn"),
preload("res://scenes/screens/levels/units/musketeer.tscn")
]

func _ready():
var max_width = 64 # Replace with your desired max width
var max_height = 64 # Replace with your desired max height
for i in range(min(get_child_count(), unit_types.size())):
var button = get_child(i)
var texture_rect = button.get_node("%TextureRect") # Replace with your actual node path
var unit_label = button.get_node("%UnitTypeLabel")
button.UnitClass = unit_scenes[i]
texture_rect.texture = unit_images[i]
texture_rect.set_size(Vector2(32, 32))

Houses

I`ve got around to reimplementing the spawning of houses inside cities. But they do not have any functionality for now. If you have an idea for a functionality for this scene feel free to share it in the comments

Money System

in the UI, you can now see the money, both players currently have. The next step will be to add the list of units the players have left on the board.

Conclusion

So this is it for today's diary. Today I have explained how I have restructured my game's code and how it affected the workflow. We have also covered other improvements like an improved buy bar or the addition of a money system.

Check out the previous entry here

Check out the next diary entry here

--

--

Andrew Lukes

Introducing Andrew Lukes: a Prague web dev & language enthusiast who shares his ideas on Medium. You can visit andrewebdev.online to see some of my projects