@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 speed at which the bullet should travel. @export var speed: float ## 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()