Ha, awesome! I updated my level-wrap-around code and this worked a lot better than I expected.
The old stuff
As you may know Defender has this level wrapping mechanic where you can fly in one direction and the level will eventually wrap around and repeat. Previously I implemented this basically by carrying two big (2 and 3 times the screen size) collision shapes around with the player (camera). One collider would detect if an enemy would exit on the left or right side and if that is the case wrap it around to the other side of the level.
The level is made out of 4 level chunks. Each chunk is the size of the screen.
And so I had I second collider that would detect if a level chunk would leave this second collider when the player moved too far into one direction.
So basically the player sits in the center and the world gets wrapped around them, meaning level chunks and enemies jump left or right off-screen.
Well, that's the short of it. All of this worked good enough so far but I didn't really like it all that much because a) it caused issues after the player was destroyed and respawned and b) it's very player-centric and I didn't like that the player (or camera actually) was lugging around these colliders and have authority over things that they shouldn't have.
The new stuff
So I changed it. The camera is just a dumb camera once again and now each level chunk has a collider that detects if something leaves to the left or right.
If an enemy or shot leaves the left-most or right-most level chunk then it will wrap around to other side of the level.
Should the player leave a level chunk then level chunks opposite of the player movement direction will wrap around to the other side of the level. Also now more than one chunk can wrap around, to make sure there is always ample space off-screen in front of the player.
A possible problem
So far so good. But initially I thought that this would still cause some headache because when I wrap around the level chunks then I would also need to wrap around the enemies that are "in" those chunks. But: Enemies and chunks are both children of the world and not of each other. Enemies are drawn over the chunks but they are not children of those chunks, meaning when I wrap a chunk it doesn't automatically wrap enemies with it.
I thought about changing the hierarchy so that enemies and shots are children of chunks but that would mean that I would need to re-parent them when they move around. And generally it would make everything ugly.
So I decided that for now it would be better to find all the enemies and shots that are "inside" a chunk once it moves and move them as well.
But...! π
Turns out I don't have to. Everything happens automagically, which is always the best.
I totally did not think about this initially but once I move a level chunk to the other side of the level that would of course automatically trigger the "level chunk exited" signal for all those enemies and shots that were "inside" the chunk. After all it doesn't matter if small collider A leaves big collider B or collider B gets pulled away from under A. The result is the same: A is no longer inside B and has exited.
And that made everything fairly simple and clean.
I made a recording with the normal game view initially and then zoomed out.
Couple of notes:
- I debug-draw the collision shapes.
- Player is invulnerable.
- When zoomed out shots leave the world at the top and bottom and player lasers fly around infinitely. This doesn't happen in the real game because they automatically get destroyed once they leave the visible screen area.
- At one point there is an enemy shooting in exactly the frame where its level chunk wraps around to the other side, leaving its shot off-screen outside the level. This also wouldn't happen in the real game because enemies don't shoot when off-screen.
- There will always be two screens in front of the player.
For those curious, this is the code. The component is attached to World. And yeah, I know that I am naughty casting the value of an enum to int, but hey, it's just so convenient... π
(Ignore the "1152" constant, this will get replaced eventually.)
class_name WrapAroundLevelComponent extends Node
enum WrapAroundSide {
left = -1,
right = 1
}
func _ready():
var world = get_parent() as World
if world:
assert(world.get_ordered_level_chunks().size() >= 3)
for chunk in world.get_ordered_level_chunks():
chunk.level_chunk_exited.connect(_on_level_chunk_exited)
func number_of_level_chunks_in_front_of_player(total_number_of_level_chunks: int) -> int:
return ceili((total_number_of_level_chunks - 1) / 2.0)
func wrap_node_around_level(node: Node2D, wrap_to_side: WrapAroundSide, number_of_level_chunks: int, level_chunk_width: int):
node.global_position.x += number_of_level_chunks * level_chunk_width * int(wrap_to_side)
func wrap_chunks_around_level(level_chunks: Array[Node], begin_index: int, end_index: int, wrap_to_side: WrapAroundSide):
for i in range(begin_index, end_index):
wrap_node_around_level(level_chunks[i], wrap_to_side, level_chunks.size(), 1152)
func _on_level_chunk_exited(level_chunk: LevelChunk, node: Node2D):
var world = get_parent() as World
var level_chunks = world.get_ordered_level_chunks()
if node is Player:
var chunk_exited_index := level_chunks.find(level_chunk)
assert(chunk_exited_index >= 0)
if node.global_position.x < level_chunk.global_position.x:
# wrap to left side
var chunk_index := chunk_exited_index - 1
var number_of_chunks_to_wrap = number_of_level_chunks_in_front_of_player(level_chunks.size()) - chunk_index
if number_of_chunks_to_wrap > 0:
wrap_chunks_around_level(level_chunks, level_chunks.size() - number_of_chunks_to_wrap, level_chunks.size(), WrapAroundSide.left)
elif node.global_position.x > level_chunk.global_position.x + 1152:
# wrap to right side
var chunk_index := chunk_exited_index + 1
var number_of_chunks_to_wrap = number_of_level_chunks_in_front_of_player(level_chunks.size()) - (level_chunks.size() - chunk_index) + 1
if number_of_chunks_to_wrap > 0:
wrap_chunks_around_level(level_chunks, 0, number_of_chunks_to_wrap, WrapAroundSide.right)
else:
if node.global_position.x < level_chunks.front().global_position.x:
wrap_node_around_level(node, WrapAroundSide.right, level_chunks.size(), 1152)
elif node.global_position.x > level_chunks.back().global_position.x + 1152:
wrap_node_around_level(node, WrapAroundSide.left, level_chunks.size(), 1152)