paratate/systems/bullets/bullet.gd

135 lines
3.7 KiB
GDScript

@tool
@icon("bullet.svg")
class_name Bullet
extends Area2D
## Emitted whenever a bullet is recycled.
signal recycled()
# cached shapes for each hitbox size
static var _hitbox_shapes: Dictionary[Vector2i, RectangleShape2D] = {}
## Base graphic/animation to display, modulated by [member color].
@export var base_graphic: AnimationStrip = null:
set(value):
base_graphic = value
queue_redraw()
_update_visibility_notifier()
## The color that [member base_graphic] will be modulated by.
@export var color: Color
## Extra graphic drawn over [member base_graphic] and not modulated by [member color].
@export var overlay_graphic: AnimationStrip = null:
set(value):
overlay_graphic = value
queue_redraw()
## Size of the bullet's collision box.
@export var hitbox_size: Vector2i = Vector2i.ZERO:
set(value):
hitbox_size = value
if not _hitbox_shapes.has(hitbox_size):
var new_shape = RectangleShape2D.new()
new_shape.size = hitbox_size
_hitbox_shapes[hitbox_size] = new_shape
_hitbox.shape = _hitbox_shapes[hitbox_size]
## The direction that the bullet is travelling.
@export_custom(0, "direction") var direction: Vector2 = Vector2.RIGHT
## If [code]true[/code], the bullet will always rotate to face toward [member direction].
@export var face_direction: bool = false
## The amount of time in seconds that the bullet has existed.
var time_elapsed: float = 0.0
## Whether the bullet has already been grazed by the player.
var grazed: bool = false
var _hitbox: CollisionShape2D = CollisionShape2D.new()
## Returns a new [Bullet], which may be sourced from the cached bullets.
@warning_ignore("shadowed_variable")
static func create(preset: BulletPreset, direction: Vector2 = Vector2.RIGHT) -> Bullet:
var bullet := Bullet.new()
if not preset.base_graphics.is_empty():
var index = randi() % preset.base_graphics.size()
bullet.base_graphic = preset.base_graphics[index]
bullet.overlay_graphic = preset.overlay_graphics[index]
if not preset.colors.is_empty():
bullet.color = preset.colors.pick_random()
bullet.hitbox_size = preset.hitbox_size
bullet.face_direction = preset.face_direction
bullet.direction = direction
bullet.time_elapsed = 0.0
bullet.grazed = false
return bullet
func _init() -> void:
monitoring = false
collision_layer = 1 << 3
collision_mask = 0
add_to_group(&"bullets")
_hitbox.debug_color.a = 0.0
add_child(_hitbox)
func _enter_tree() -> void:
_update_visibility_notifier()
func _physics_process(_delta: float) -> void:
if not Engine.is_editor_hint():
queue_redraw()
if face_direction:
rotation = direction.angle()
func _draw() -> void:
if base_graphic:
base_graphic.draw(self, time_elapsed, color)
if overlay_graphic:
overlay_graphic.draw(self, time_elapsed, Color.WHITE)
func _get_configuration_warnings() -> PackedStringArray:
return []
## Removes the bullet from the scene tree and returns it to the bullet cache to be
## re-used later.
func recycle() -> void:
queue_free()
recycled.emit()
# sets the canvas item up to notify when it leaves the screen
# this essentially mimics `VisibleOnScreenNotifier` without an additional node
func _update_visibility_notifier() -> void:
if not base_graphic or not base_graphic.texture or not is_inside_tree() or Engine.is_editor_hint():
return
# the visibility rect is set to twice the size of the texture to add a little margin
# (func(): pass) is the cleanest way i could think of to have a callback that does nothing
RenderingServer.canvas_item_set_visibility_notifier(
get_canvas_item(), true,
Rect2(-base_graphic.texture.get_size(), base_graphic.texture.get_size() * 2.0),
(func(): pass), _on_screen_exited
)
# called when the bullet leaves the screen.
func _on_screen_exited() -> void:
recycle()