yes, they canjj st!

This commit is contained in:
Haze Weathers 2025-12-14 17:06:42 -06:00
parent 6b92473eeb
commit a6421235f8
22 changed files with 510 additions and 14 deletions

View file

@ -0,0 +1,12 @@
@abstract
class_name BulletBehavior
extends Resource
## Called when a bullet is spawned with this behavior in order to set up
## behavior-specific state.
@warning_ignore("unused_parameter")
func init_bullet(bullet: Bullet) -> void: pass
## Called to process a tick of a bullet's movement.
@abstract func process_bullet(bullet: Bullet, delta: float) -> void

View file

@ -0,0 +1 @@
uid://djcajenyac4sd

View file

@ -0,0 +1,15 @@
class_name SimpleLinearBehavior
extends BulletBehavior
## Makes bullets move in [member Bullet.direction], potentially accelerating.
## Initial speed of the bullet when it is spawned.
@export_custom(0, "suffix:px/s") var initial_speed: float = 0.0
## Rate at which the bullet will speed up.
@export_custom(0, "suffix:px/s²") var acceleration: float = 0.0
func process_bullet(bullet: Bullet, delta: float) -> void:
var speed = initial_speed + acceleration * bullet.time_elapsed
bullet.position += bullet.direction * speed * delta

View file

@ -0,0 +1 @@
uid://dntp60my5f65m

View file

@ -1,17 +1,22 @@
@tool
@icon("bullet.svg")
class_name Bullet
extends Area2D
static var _hitbox_shapes: Dictionary[Vector2i, RectangleShape2D] = {}
## The number of bullets to allocate at startup.
const INITIAL_ALLOCATED_BULLETS: int = 2000
@export var texture: Texture2D:
## Texture to draw for the bullet.
@export var texture: Texture2D = null:
set(value):
texture = value
queue_redraw()
_update_visibility_notifier()
@export var hitbox_size: Vector2i:
## 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):
@ -20,18 +25,100 @@ static var _hitbox_shapes: Dictionary[Vector2i, RectangleShape2D] = {}
_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
static var _cached_bullets: Array[Bullet] = []
static var _hitbox_shapes: Dictionary[Vector2i, RectangleShape2D] = {}
var _hitbox: CollisionShape2D = CollisionShape2D.new()
## Returns a new [Bullet], which may be sourced from the cached bullets.
@warning_ignore("shadowed_variable")
static func create(
texture: Texture2D = null,
hitbox_size: Vector2i = Vector2i.ZERO,
direction: Vector2 = Vector2.RIGHT,
face_direction: bool = false,
) -> Bullet:
var bullet: Bullet = _cached_bullets.pop_back()
if not bullet:
bullet = Bullet.new()
bullet.texture = texture
bullet.hitbox_size = hitbox_size
bullet.direction = direction
bullet.face_direction = face_direction
bullet.time_elapsed = 0.0
return bullet
## Removes the bullet from the scene tree and returns it to the bullet cache to be
## re-used later.
func recycle() -> void:
get_parent().remove_child(self)
_cached_bullets.append(self)
static func _static_init() -> void:
for _i in INITIAL_ALLOCATED_BULLETS:
_cached_bullets.append(Bullet.new())
func _init() -> void:
monitoring = false
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():
time_elapsed += delta
if face_direction:
rotation = direction.angle()
func _draw() -> void:
draw_texture(texture, -texture.get_size() * 0.5)
if texture:
draw_texture(texture, -texture.get_size() * 0.5)
func _get_configuration_warnings() -> PackedStringArray:
return []
# 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 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(-texture.get_size(), texture.get_size() * 2.0),
(func(): pass), _on_screen_exited
)
# called when the bullet leaves the screen.
func _on_screen_exited() -> void:
recycle()

View file

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="16"
height="16"
version="1.1"
id="svg1"
sodipodi:docname="bullet.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<linearGradient
id="a"
x2="0"
y1="2"
y2="14"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4)">
<stop
offset="0"
stop-color="#ff8dbc"
id="stop1" />
<stop
offset=".4"
stop-color="#7260ff"
id="stop2" />
<stop
offset=".6"
stop-color="#7260ff"
id="stop3" />
<stop
offset="1"
stop-color="#74c9ff"
id="stop4" />
</linearGradient>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
inkscape:zoom="10.995366"
inkscape:cx="8.7309506"
inkscape:cy="10.049688"
inkscape:window-width="1358"
inkscape:window-height="742"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 1.3983532,8.3266862 6.414829,13.926473 7.737001,12.526527 2.7594126,7.160064 Z"
id="path2"
sodipodi:nodetypes="ccccc" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 3.4711505,6.74088 8.3320767,11.951793 13.465215,7.2075289 C 14.904049,5.69092 16.498433,3.3965627 14.554063,1.5299671 13.270778,-0.10330412 11.326407,0.36334478 9.1098249,2.0743908 Z"
id="path3"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 3.2131098,11.837773 1.056944,14.71266 4.1432206,12.810161 Z"
id="path5"
sodipodi:nodetypes="cccc" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 4.8196647,13.40205 2.5789434,15.219994 5.4538311,14.247606 Z"
id="path6" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:3.78"
d="M 1.4797216,10.019829 0.42277761,12.979273 2.3252768,10.992217 Z"
id="path7"
sodipodi:nodetypes="cccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://73kflfycjlft"
path="res://.godot/imported/bullet.svg-bd5de27a67ea13ec7922f62f013cca2b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://systems/bullets/bullet.svg"
dest_files=["res://.godot/imported/bullet.svg-bd5de27a67ea13ec7922f62f013cca2b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,16 @@
class_name BulletPreset
extends Resource
@export var behavior: BulletBehavior = null
@export var pattern: BulletSpawnPattern = null
@export var textures: Array[Texture2D] = []
@export var hitbox_size: Vector2i = Vector2i.ZERO
@export var face_direction: bool = false
@export var rounds: int = 1
@export var round_delay: float = 0.0

View file

@ -0,0 +1 @@
uid://vus1a0flwtnm

View file

@ -0,0 +1,31 @@
class_name BulletSet
extends Node2D
@export var preset: BulletPreset
func _ready() -> void:
for _n in preset.rounds:
preset.pattern.spawn_bullets(self, preset)
await get_tree().create_timer(preset.round_delay, false, true).timeout
func _physics_process(delta: float) -> void:
for bullet in get_children():
if bullet is Bullet:
preset.behavior.process_bullet(bullet, delta)
else:
push_error("BulletSet does not support having non-bullet children. Removing child: ", bullet)
bullet.queue_free()
func add_bullet(bullet: Bullet) -> void:
preset.behavior.init_bullet(bullet)
add_child(bullet)
func add_bullets(new_bullets: Array[Bullet]) -> void:
for bullet in new_bullets:
preset.behavior.init_bullet(bullet)
add_child(bullet)

View file

@ -0,0 +1 @@
uid://cj2fj7snls8aa

View file

@ -0,0 +1,6 @@
@abstract
class_name BulletSpawnPattern
extends Resource
@abstract func spawn_bullets(bullet_set: BulletSet, preset: BulletPreset) -> void

View file

@ -0,0 +1 @@
uid://bhy0mkwfsi5j8

View file

@ -0,0 +1,21 @@
class_name RingPattern
extends BulletSpawnPattern
@export var bullet_count: int
@export var distance_offset: float
@export_custom(0, "radians_as_degrees") var angle_offset: float
@export_custom(0, "radians_as_degrees") var direction_rotation: float
func spawn_bullets(bullet_set: BulletSet, preset: BulletPreset) -> void:
for i in bullet_count:
var angle = (float(i) / float(bullet_count)) * TAU + angle_offset
var bullet = Bullet.create(
preset.textures.pick_random(),
preset.hitbox_size,
Vector2.from_angle(angle + direction_rotation),
preset.face_direction
)
bullet.position = Vector2.from_angle(angle) * distance_offset
bullet_set.add_bullet(bullet)

View file

@ -0,0 +1 @@
uid://dtuc6qerbfset