Base Ranged Combat State
Base Ranged Combat State
base_ranged_combat_state.gd
is the base class for all ranged combat states. It contains common functions that all ranged combat states require. It extends from BaseState
. The BaseRangedCombatState
class is designed to be inherited by specific ranged combat states, such as MinigunAttackState
, BowWindupState
, etc. This allows for a modular and reusable design, where each specific state can implement its own behavior while still sharing common functionality.
Design Philosophy
The ranged weapon system is built on a state machine architecture. This design choice offers several advantages:
-
Separation of Concerns: Each weapon state is responsible for a specific aspect of the weapon’s behavior, making the code easier to understand and modify. It also makes it clear where to look for specific functionality, such as attack logic or cooldown management.
- For example, the
MinigunAttackState
handles the logic for firing the minigun, while theBowWindupState
manages the bow’s charging behavior.
- For example, the
-
Reusability: Common functionality is implemented in the base class, reducing code duplication and ensuring consistent behavior across different weapons.
- For instance, the
process_hit
function is defined in the base class and can be reused by all ranged combat states, ensuring that hit detection is the same for each hitscan weapon.
- For instance, the
-
Control: The state-based approach allows for precise control over animation timing, effects, sound, and other aspects of weapon behavior.
Class Definition
class_name BaseRangedCombatStateextends BaseState
@export var ANIMATION_NAME: String@export var state_type: WeaponEnums.WeaponState
var weapon: RangedWeaponvar transition_signal : Signalvar origin_entity : PhysicsBody3D ## The entity that is using the weapon
func setup(_weapon_node: RangedWeapon, _transition_signal : Signal) -> void: if not _weapon_node: print("Weapon does not exist! Please provide a valid weapon node.") return
if not _transition_signal: print("Transition signal does not exist! Please provide a valid signal.") return
weapon = _weapon_node transition_signal = _transition_signal
func enter(_previous_state, _information: Dictionary = {}) -> void: pass
func process_hit(raycast: RayCast3D) -> void: # make the raycast immediately check for collisions raycast.force_raycast_update()
if raycast.is_colliding(): var collider: Object = raycast.get_collider() print("Raycast hit: " + collider.name)
if collider is PhysicsBody3D: DebugDrawer.draw_debug_impact(raycast.get_collision_point(), collider) if collider == origin_entity: print("Hit self") return print("Colliding with:" + collider.name) # TODO: hit marker SignalManager.weapon_hit_target.emit(collider, weapon.current_weapon.damage)
Key Components
- Animation Name: The name of the animation to be played during the attack.
- State Type: The type of state, defined in
WeaponEnums.WeaponState
. - Weapon: The weapon instance associated with this state, passed on from the state machine.
- Transition Signal: A signal used to transition between states.
- This signal is emitted when the state needs to change, allowing for smooth transitions between different weapon states.
- Origin Entity: The entity that is using the weapon.
- Setup Function: Initializes the weapon and transition signal.
- Enter Function: Called when entering this state. Can be overridden in child classes.
- Process Hit Function: Processes the hit from the raycast, checking for collisions and emitting signals as necessary.
- Only used by hitscan weapons
Implementation Benefits
By moving state management to the weapon itself, the system allows for more complex weapon mechanics and behaviors.
-
Decoupled Weapon Logic: The weapon’s behavior is decoupled from the entity using it, allowing the same weapon to be used by different entities (players, enemies) with minimal code changes.
- This allows for more complex weapon mechanics, such as different firing modes or special abilities, without needing to modify the entity’s code.
- For example, a player can use a bow and arrow, while an enemy can use the same bow with different attack patterns set in their AI.
- This allows for more complex weapon mechanics, such as different firing modes or special abilities, without needing to modify the entity’s code.
Usage
In order to correctly use the ranged weapon, the entity needs to call the ranged_weapon_handler.gd
script. This script is responsible for managing the ranged weapon’s state machine and handling the transitions between different states.
## Handles input transitions for ranged weapon state machineclass_name RangedWeaponHandler extends Node
@export var state_machine: RangedWeaponStateMachine
## Called when attack action is initiated (button pressed)func start_use() -> void: match state_machine.current_state.state_type: WeaponEnums.WeaponState.COOLDOWN: print("Attack not allowed during cooldown") return WeaponEnums.WeaponState.IDLE: print("going to windup") state_machine.combat_transition_state.emit(WeaponEnums.WeaponState.WINDUP, {}) WeaponEnums.WeaponState.ATTACKING: # Allow continuous fire if weapon supports it if weapon_supports_hold_fire(): pass else: print("Cannot attack again during attack") _: pass
## Called when attack action is released (button released)func end_use() -> void: print("Stopping weapon") match state_machine.current_state.state_type: WeaponEnums.WeaponState.WINDUP: # Cancel windup if released early state_machine.combat_transition_state.emit(WeaponEnums.WeaponState.IDLE, {}) WeaponEnums.WeaponState.ATTACKING: # Cancel attack if released early if weapon_supports_early_release(): state_machine.combat_transition_state.emit(WeaponEnums.WeaponState.COOLDOWN, {}) _: pass
## Helper to check weapon's hold capabilityfunc weapon_supports_hold_fire() -> bool: return state_machine.weapon.current_weapon.allow_continuous_fire
## Helper to check if weapon allows early releasefunc weapon_supports_early_release() -> bool: return state_machine.weapon.current_weapon.allow_early_release
Example Usage
The following script is how the player entity would call the RangedWeaponHandler
. The enemy would call the same functions, but based on AI instead of input actions.
extends Node
@onready var current_weapon : RangedWeapon = $"../CurrentRangedWeapon".current_weapon
func _input(event: InputEvent) -> void: if event.is_action_pressed("attack_secondary"): _start_firing() elif event.is_action_released("attack_secondary"): _stop_firing()
func _start_firing() -> void: if not is_instance_valid(current_weapon): push_warning("No valid ranged weapon equipped") return
if current_weapon.handler: current_weapon.handler.start_use()
func _stop_firing() -> void: if not is_instance_valid(current_weapon): return
if current_weapon.handler: current_weapon.handler.end_use()
Extending the System
When creating a new ranged weapon, you should consider:
- Required States: Implement at minimum an idle state, attack state, and cooldown state for your weapon.
- The idle state is the default state when the weapon is not in use.
- The attack state handles the weapon’s firing logic.
- The cooldown state manages the time between attacks.
- The windup state is optional but can be used for weapons that require a delay before firing, like the minigun.
- Optional Windup: Add a windup state for weapons that should not immediately fire after pressing the attack button.
- This state can be used for weapons that require a charge-up time, like bows or crossbows.
- The windup state can also be used for an aiming state, where the player can aim before firing.
- Projectile vs. Hitscan: Decide if your weapon uses instant hit detection (hitscan) or spawns physical projectiles.
- Configuration Properties: Define properties like damage, etc. in a resource file.
- Sound and VFX: Each state can trigger sounds and visual effects to enhance player feedback.