Replace the firing bullet emitter with static bullet obstacles ringed around (12,12)
The user did not want anything firing: the bullets should simply exist in the air as fixed obstacles. bullet_ring (the emitter) is deleted and bullet_field takes its place. bullet_field generates concentric rings of frozen bullets centred on itself, radii given in cells (default 5/10/15), and skips bullets that fall outside the level bounds or land inside solid tiles so nothing is buried in the terrain. Each bullet is a StaticBody2D on the world layer, so it blocks the player and can be stood on; damage defaults to 0 (pure obstacle) and can be raised to turn them into hazards.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
extends Node2D
|
||||
|
||||
## 静态弹幕障碍:以自己为圆心,一圈一圈地摆上**固定不动、不会发射**的子弹。
|
||||
## 默认只是纯障碍(实心,撞得到、能踩上去),把 damage 调大于 0 就变成碰到扣血。
|
||||
## 落在实心地砖里的子弹会自动跳过(不然会埋在地里)。
|
||||
|
||||
const BULLET_SCRIPT := preload("res://scripts/bullet.gd")
|
||||
|
||||
@export var enabled: bool = true
|
||||
## 每一圈的半径(单位:格)。默认 5 格、10 格、15 格
|
||||
@export var radii_cells: Array[int] = [5, 10, 15]
|
||||
## 每圈摆几颗
|
||||
@export var per_ring: int = 20
|
||||
## 一格多少像素(图块是 18)
|
||||
@export var cell_size: float = 18.0
|
||||
## 起始角度偏移(度),用来微调子弹摆在哪个方向
|
||||
@export var angle_offset_deg: float = 0.0
|
||||
## 子弹半径
|
||||
@export var bullet_radius: int = 3
|
||||
@export var bullet_color: Color = Color(1.0, 0.45, 0.85)
|
||||
## 实心障碍:玩家撞得到、也能踩上去
|
||||
@export var solid: bool = true
|
||||
## 碰到扣几颗心。0 = 只是障碍不扣血
|
||||
@export var damage: int = 0
|
||||
## 超出这个范围的子弹不生成(默认整个关卡 468x270)
|
||||
@export var bounds: Rect2 = Rect2(0, 0, 468, 270)
|
||||
## 最多铺几圈
|
||||
@export var max_rings: int = 3
|
||||
## 跳过埋在地砖里的子弹
|
||||
@export var skip_solid_ground: bool = true
|
||||
|
||||
var made: int = 0
|
||||
var skipped_out: int = 0
|
||||
var skipped_ground: int = 0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not enabled:
|
||||
return
|
||||
# 等一帧,确保 TileMapLayer 都已经就绪
|
||||
_build.call_deferred()
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
var layers := _tile_layers()
|
||||
var tex := BULLET_SCRIPT.circle_texture(bullet_radius, bullet_color)
|
||||
made = 0
|
||||
skipped_out = 0
|
||||
skipped_ground = 0
|
||||
for i in mini(radii_cells.size(), max_rings):
|
||||
var radius: float = float(radii_cells[i]) * cell_size
|
||||
for k in per_ring:
|
||||
var ang := deg_to_rad(angle_offset_deg + 360.0 * float(k) / float(per_ring))
|
||||
var pos := global_position + Vector2(cos(ang), sin(ang)) * radius
|
||||
if not bounds.has_point(pos):
|
||||
skipped_out += 1
|
||||
continue
|
||||
if skip_solid_ground and _in_solid(layers, pos):
|
||||
skipped_ground += 1
|
||||
continue
|
||||
_make(pos, tex)
|
||||
made += 1
|
||||
|
||||
|
||||
func _tile_layers() -> Array:
|
||||
var root: Node = owner if owner != null else get_tree().current_scene
|
||||
if root == null:
|
||||
return []
|
||||
return root.find_children("*", "TileMapLayer", true, false)
|
||||
|
||||
|
||||
## 这个点是不是埋在实心地砖里
|
||||
func _in_solid(layers: Array, pos: Vector2) -> bool:
|
||||
for l in layers:
|
||||
var cell: Vector2i = l.local_to_map(l.to_local(pos))
|
||||
var sid: int = l.get_cell_source_id(cell)
|
||||
if sid == -1:
|
||||
continue
|
||||
var source := l.tile_set.get_source(sid) as TileSetAtlasSource
|
||||
if source == null:
|
||||
continue
|
||||
var data: TileData = source.get_tile_data(l.get_cell_atlas_coords(cell), 0)
|
||||
if data != null and data.get_collision_polygons_count(0) > 0:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _make(pos: Vector2, tex: Texture2D) -> void:
|
||||
var body := StaticBody2D.new()
|
||||
if solid:
|
||||
body.collision_layer = 1
|
||||
body.collision_mask = 1
|
||||
else:
|
||||
body.collision_layer = 0
|
||||
body.collision_mask = 0
|
||||
add_child(body)
|
||||
body.global_position = pos
|
||||
|
||||
var sprite := Sprite2D.new()
|
||||
sprite.texture = tex
|
||||
sprite.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||
body.add_child(sprite)
|
||||
|
||||
if solid:
|
||||
var shape := CollisionShape2D.new()
|
||||
var circle := CircleShape2D.new()
|
||||
circle.radius = float(bullet_radius)
|
||||
shape.shape = circle
|
||||
body.add_child(shape)
|
||||
|
||||
if damage > 0:
|
||||
var area := Area2D.new()
|
||||
area.collision_layer = 4
|
||||
area.collision_mask = 2
|
||||
var hit := CollisionShape2D.new()
|
||||
var circle2 := CircleShape2D.new()
|
||||
circle2.radius = float(bullet_radius) + 1.0
|
||||
hit.shape = circle2
|
||||
area.add_child(hit)
|
||||
body.add_child(area)
|
||||
area.body_entered.connect(_on_hit)
|
||||
|
||||
|
||||
func _on_hit(body: Node2D) -> void:
|
||||
if body.has_method("hurt"):
|
||||
body.hurt(damage)
|
||||
@@ -1,76 +0,0 @@
|
||||
extends Node2D
|
||||
|
||||
## 固定弹幕源:每隔一段时间以自己为中心,按**固定角度**围一圈发射子弹。
|
||||
## 每一波的角度都算死(可选每波整体旋转),所以弹道可以背下来,
|
||||
## 玩家靠走位找空隙,或者用「扔钥匙 + W 传送」直接穿过去。
|
||||
|
||||
const BULLET := preload("res://scenes/prefabs/enemies/bullet.tscn")
|
||||
const BULLET_SCRIPT := preload("res://scripts/bullet.gd")
|
||||
|
||||
@export var enabled: bool = true
|
||||
## 发射间隔(秒)
|
||||
@export var interval: float = 2.0
|
||||
## 开场后第一次发射前的延迟
|
||||
@export var start_delay: float = 1.0
|
||||
## 每波几颗
|
||||
@export var count: int = 12
|
||||
## 扇形张开角度:360 = 围一整圈
|
||||
@export var arc_deg: float = 360.0
|
||||
## 整圈的中心朝向(度):0=右 90=下 180=左 270=上
|
||||
@export var base_angle_deg: float = 0.0
|
||||
## 每发一波整体旋转多少度(0 = 弹道完全固定,可以背板)
|
||||
@export var rotate_per_shot_deg: float = 0.0
|
||||
@export var bullet_speed: float = 55.0
|
||||
@export var bullet_color: Color = Color(1.0, 0.45, 0.85)
|
||||
## 中心的圆点半径(只是给玩家一个视觉提示)
|
||||
@export var core_radius: int = 4
|
||||
|
||||
var _timer: float = 0.0
|
||||
var _angle: float = 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("emitter")
|
||||
_timer = start_delay
|
||||
_angle = base_angle_deg
|
||||
var core := get_node_or_null("Core") as Sprite2D
|
||||
if core != null:
|
||||
core.texture = BULLET_SCRIPT.circle_texture(core_radius, bullet_color)
|
||||
|
||||
|
||||
func _physics_process(delta: float) -> void:
|
||||
if not enabled:
|
||||
return
|
||||
_timer -= delta
|
||||
if _timer > 0.0:
|
||||
return
|
||||
_timer = maxf(interval, 0.05)
|
||||
fire()
|
||||
|
||||
|
||||
func fire() -> void:
|
||||
var n: int = maxi(count, 1)
|
||||
var base := deg_to_rad(_angle)
|
||||
var full_circle := is_equal_approx(arc_deg, 360.0)
|
||||
var step := deg_to_rad(arc_deg) / float(n) if full_circle else deg_to_rad(arc_deg)
|
||||
for i in n:
|
||||
var ang: float
|
||||
if full_circle:
|
||||
ang = base + step * float(i)
|
||||
elif n <= 1:
|
||||
ang = base
|
||||
else:
|
||||
ang = base + step * (float(i) / float(n - 1) - 0.5)
|
||||
_spawn(ang)
|
||||
if not is_zero_approx(rotate_per_shot_deg):
|
||||
_angle = fmod(_angle + rotate_per_shot_deg, 360.0)
|
||||
|
||||
|
||||
func _spawn(ang: float) -> void:
|
||||
var bullet := BULLET.instantiate()
|
||||
# 这两个属性必须在 add_child 之前设好,子弹的 _ready 会拿它们生成贴图
|
||||
bullet.bullet_color = bullet_color
|
||||
bullet.speed = bullet_speed
|
||||
get_parent().add_child(bullet)
|
||||
bullet.global_position = global_position
|
||||
bullet.launch(Vector2(cos(ang), sin(ang)), bullet_speed)
|
||||
Reference in New Issue
Block a user