extends Node2D ## 静态弹幕障碍:以自己为圆心,一圈一圈地摆上**固定不动、不会发射**的子弹。 ## ## 两种形态: ## - solid = true :实心障碍,挡路、能踩上去(注意:会连小怪一起挡住) ## - solid = false:可以穿透,只做伤害判定 —— 配合 damage > 0 就是普通弹幕 ## ## 落在实心地砖里、或跑出关卡范围的子弹会自动跳过。 const BULLET_SCRIPT := preload("res://scripts/bullet.gd") @export var enabled: bool = true ## 每一圈的半径(单位:格)。默认 3 格、6 格、9 格 @export var radii_cells: Array[int] = [3, 6, 9] ## 每圈摆几颗 @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 = false ## 碰到扣几颗心。0 = 不扣血 @export var damage: int = 1 ## 超出这个范围的子弹不生成(默认整个关卡 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 var _hurt_areas: Array = [] 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 _hurt_areas.clear() 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 ## 每帧检查一次:站在子弹里也会持续扣血 ## (不能只靠 body_entered —— 玩家本来就在子弹里的话不会触发) func _physics_process(_delta: float) -> void: if damage <= 0 or _hurt_areas.is_empty(): return for area in _hurt_areas: if not is_instance_valid(area): continue for body in area.get_overlapping_bodies(): if body.has_method("hurt"): body.hurt(damage) 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) _hurt_areas.append(area)