Skip to content

Minigun

Weapon Description

The minigun is a powerful ranged weapon that fires a high volume of bullets in quick succession. Each bullet deals minimal damage, but the rapid fire rate makes it very effective against enemies. The weapon has a long cooldown time to balance its power.

Attack State Mechanics

The attack state manages both visual representation and damage functionality. When activated, it rapidly fires projectiles in a controlled spiral pattern, simulating the “spray” of a multi-barreled machine gun.

Spiral Firing Pattern

The minigun fires bullets in a spiral pattern that:

  • Creates an unpredictable firing pattern
  • Balances high fire rate with controlled spread
  • Visually communicates the weapon’s power

The pattern uses a rotating offset around a central point (barrel_radius), simulating the multiple rotating barrels.

Bullet Visualization

The weapon creates tracer effects using:

  • ImmediateMesh for efficient rendering
  • Cylindrical geometry for bullet trajectories
  • Timed self-destruction to prevent memory leaks
  • Raycasts with collision masks for hit detection

Processing Separation

The minigun separates:

  1. Visual Phase: Creates immediate feedback when firing
  2. Physics Phase: Handles collision detection at fixed intervals. In order for raycasts to accurately detect collisions, they need to be updated in the physics process

This ensures responsive visuals without sacrificing physics accuracy.

Automatic Timing Control

The minigun uses timers to enforce attack duration limits and transitions to a cooldown state when expired. This cooldown balances the weapon’s power.

Attack State Code

extends BaseRangedCombatState
@export var attack_origin: Node3D
@export var barrel_radius: float = 0.2 # Radius of minigun barrel arrangement
@export var spiral_spread: float = 25.0 # Degrees between each bullet's angle
@export var max_spread_angle: float = 45.0 # Max spray angle
var _fire_timer: float = 0.0
var _current_angle: float = 0.0
var _angle_direction: int = 1
var _ray_queue: Array[RayRequest] = []
@onready var attack_duration_timer : Timer = $AttackDurationTimer
func enter(_previous_state, _info: Dictionary = {}) -> void:
_fire_timer = 0.0
_current_angle = 0.0
_angle_direction = 1
attack_duration_timer.start(weapon.current_weapon.attack_duration)
func process(delta: float) -> void:
_fire_timer += delta
var fire_interval: float = weapon.current_weapon.fire_rate_per_second
while _fire_timer >= fire_interval:
_fire_timer -= fire_interval
_fire_bullet()
func physics_process(_delta: float) -> void:
# loop through raycast queue, create raycasts and check for collisions
for ray_param in _ray_queue:
var origin: Vector3 = ray_param.origin
var direction: Vector3 = ray_param.direction
var max_range: float = ray_param.max_range
var raycast: RayCast3D = _create_raycast(origin, direction, max_range)
raycast.force_raycast_update()
process_hit(raycast)
_ray_queue.clear()
func exit() -> void:
# Reset the angle to the initial position
_current_angle = 0.0
_angle_direction = 1
attack_duration_timer.stop()
# Clear ray cast queue, allows existing raycasts to still be processed
_ray_queue.clear()
# The visualization should start immediatly for game feel, but Raycasts need to be processed in physics_process to work
func _fire_bullet() -> void:
# Calculate spawn position with rotating offset
var angle_rad : float = deg_to_rad(_current_angle)
var offset := Vector3(
cos(angle_rad) * barrel_radius,
sin(angle_rad) * barrel_radius,
0
)
var fire_direction: Vector3 = attack_origin.global_basis.z.normalized()
var max_range: float = weapon.current_weapon.max_range
var origin: Vector3 = attack_origin.global_position + attack_origin.global_transform.basis * offset
# Store raycast parameters for physics processing
_ray_queue.append(RayRequest.new(origin, fire_direction, max_range))
# Visualize the trajectory
_create_trajectory_visualization(origin, fire_direction, max_range)
_update_spiral_angle()
func _create_raycast(origin: Vector3, direction: Vector3, max_range: float) -> RayCast3D:
var raycast := RayCast3D.new()
raycast.enabled = true
raycast.target_position = direction * max_range
raycast.collision_mask = 0b0110 # check for collisions on layer 2 (player) and layer 3 (enemy)
# Add to physics space first
get_tree().root.add_child(raycast)
raycast.global_position = origin
# Creating a timer to automatically remove the raycast
var timer := Timer.new()
raycast.add_child(timer)
timer.wait_time = 0.1
timer.one_shot = true
timer.timeout.connect(func():
if is_instance_valid(raycast) and raycast.is_inside_tree():
raycast.queue_free()
)
timer.start()
return raycast
func _create_trajectory_visualization(origin: Vector3, direction: Vector3, max_range: float) -> void:
var trajectory_mesh := ImmediateMesh.new()
var mesh_instance := MeshInstance3D.new()
mesh_instance.mesh = trajectory_mesh
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
# Add to scene root but position in world space
get_tree().current_scene.add_child(mesh_instance)
mesh_instance.global_position = origin
trajectory_mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)
trajectory_mesh.surface_set_color(Color(1.0, 0.5, 0.0, 0.3)) # Tracers
var radius := 0.1 # Tracer thickness
var segments := 6
var start_verts: Array[Vector3] = []
var end_verts: Array[Vector3] = []
var ortho := _find_orthogonal_vector(direction)
for i in segments:
var angle := float(i) / segments * TAU
var circle_vec := ortho.rotated(direction, angle) * radius
start_verts.append(circle_vec)
end_verts.append(direction * max_range + circle_vec)
_generate_cylinder_geometry(trajectory_mesh, start_verts, end_verts, segments)
trajectory_mesh.surface_end()
# Creating a timer to automatically remove the mesh
var timer := Timer.new()
mesh_instance.add_child(timer)
timer.wait_time = 0.15
timer.one_shot = true
timer.timeout.connect(func():
mesh_instance.queue_free()
timer.queue_free()
)
timer.start()
func _find_orthogonal_vector(direction: Vector3) -> Vector3:
return (
Vector3.RIGHT
if abs(direction.dot(Vector3.UP)) < 0.9
else Vector3.FORWARD
).cross(direction).normalized()
func _generate_cylinder_geometry(
mesh: ImmediateMesh,
start_verts: Array[Vector3],
end_verts: Array[Vector3],
segments: int
) -> void:
for i in segments:
var next_i := (i + 1) % segments
# Side triangles
mesh.surface_add_vertex(start_verts[i])
mesh.surface_add_vertex(end_verts[i])
mesh.surface_add_vertex(start_verts[next_i])
mesh.surface_add_vertex(start_verts[next_i])
mesh.surface_add_vertex(end_verts[i])
mesh.surface_add_vertex(end_verts[next_i])
func _update_spiral_angle() -> void:
_current_angle += spiral_spread * _angle_direction
_current_angle = clamp(_current_angle, -max_spread_angle, max_spread_angle)
_angle_direction *= -1 if abs(_current_angle) >= max_spread_angle else 1
func _on_attack_duration_timer_timeout() -> void:
transition_signal.emit(WeaponEnums.WeaponState.COOLDOWN, {})

Code Features

State Management

  • Built on a base ranged combat class for reusing common weapon features
  • Uses timers to control attack duration
  • Handles state transitions

Performance Optimization

  • Separates visual effects from hit detection
  • Auto-removes visual effects when not needed
  • Generates geometry on demand

Mathematics

  • Creates spiral patterns with circular calculations
  • Uses angle limits to create spray pattern

Godot Engine Features

  • Uses Godot’s built-in collision system with RayCast3D
  • Filters which objects can be hit using collision masks
  • Creates and removes objects in the game world as needed
  • Uses Godot’s timer system for both gameplay and cleanup tasks