Open Xrayez opened 2 years ago
Not sure how related it to https://github.com/godotengine/godot-proposals/issues/3359 which has a MultiplayerSpawn node.
Not sure how related it to #3359 which has a MultiplayerSpawn node.
It's only related by the fact that MultiplayerSpawn
node has Spawn
word in it. What's proposed here is a completely different set of use cases. Updated the proposal to signify this.
After working on the implementation a bit, I figured that:
Spawner.object
cannot really accept a script, because some scripts may have required arguments to pass in _init()
, and there's no API to workaround this. However, this would work fine with scripts that don't require passing arguments to constructor. Would be nice to have #1935 implemented in the first place, or similar.
Accepting Object/Node
instances won't work reliably either when exposed as a property (crashes the editor).
Due to this, I think that passing a scene would be enough, therefore I'll likely just rename the property to Spawner.scene
that will only accept PackedScene
, or could accept general-purpose Resource
that accepts both PackedScene
and Script
.
Regarding time: there are actually two approaches:
So, I think that it makes sense to add both rate
and step
properties. A rate
would specify how many objects are spawned per time step, which is 1 second by default. The step
would allow to configure the default.
Accordingly, amount/number
would have to be renamed to max_objects/limit
(before spawning stops). But note that the limit would be different from the concept of "maximum allowed objects at the same time", this will be rather controlled by lifetime
property.
I have created initial implementation for Spawner2D
class in Goost, see goostengine/goost#165.
It's nothing that is too difficult to make currently, however this would be a very commonly used feature and would make things a fair bit easier.
Also, another thing that could be added to the spawning node, is the angle of the spawned object, for things like Bullet spread. This would also make the workflow a bit more similar to particles, just for creating children instead which is an added bonus.
Also, another thing that could be added to the spawning node, is the angle of the spawned object, for things like Bullet spread. This would also make the workflow a bit more similar to particles, just for creating children instead which is an added bonus.
I see what you mean, however, consider this script:
extends Node2D
var direction = Vector2()
func _ready():
direction = Random2D.direction
rotation = Random2D.rotation
func _process(delta):
global_position += direction * delta * 100
In this case, Spawner2D
would override rotation
of the spawned node, even if the node itself randomizes the rotation first. So I'm not sure about this, because setting rotation should be a responsibility of a bullet itself, especially when bullet may change direction in the process.
However, the issue with overriding can be solved by connecting the node_spawned
signal:
func _on_node_spawned(node):
node.rotation = Random2D.rotation
The signal approach would also be superior if you switch spawned scene/script at run-time, so the same randomization could be applied to any kind of bullet, for instance.
Note that this would only be needed if you're not interested in synchronizing transform of the spawned node from Spawner2D
by default.
So I guess it's fine to synchronize global transform (and not just position) upon spawning by default because there's a way to override it via signal.
Alternatively, we could add sync_position
, sync_rotation
, sync_scale
as properties, similarly to RemoteTransform2D.update_*
properties, which is the ultimate solution for spawning in both local and global coordinates.
Alternatively, we could add
sync_position
,sync_rotation
,sync_scale
as properties, similarly toRemoteTransform2D.update_*
properties, which is the ultimate solution for spawning in both local and global coordinates.
I have added ability to modify transform properties upon spawning:
Let me know if Synchronize
would be a better word to describe this behavior. But unlike RemoteTransform2D
, Spawner2D
will only modify transform upon spawning, and won't continuously update the transform.
I've added spawn_path
NodePath
property to customize to which node the spawner is going to instantiate new nodes in the scene. For instance, this is needed if you have a system which relies on node hierarchy. In Goost, destructible terrain could be implemented by constructing nested PolyNode2D
s:
Those spawners that create explosions at run-time (delayed), are going to sequentially add PolyNode2D
with OP_DIFFERENCE
:
Just wanted to document this use case.
@anunknowperson I've noticed your 👎 on the proposal, could you explain the reason why you dislike this proposal? Is there anything that you don't like regarding the implementation, or you simply don't want to see this feature to be integrated in Godot specifically?
I'm looking forward to your feedback, thank you.
It just seems to me that from the point of view of implementation, this function is very simple. However, in real conditions, people will still have to write their own implementation that is more suitable for their needs.
I believe the proposed implementation should be useful for at least 70% of use cases. The "Enemy spawning" is probably one the most common use cases in game development in general, but I'm not sure what percentage of people using Godot make these kind of games, since this requires AI programming, and there are not a lot of features in Godot that facilitate AI development, unfortunately.
The current approach is "data-driven" as well: you don't even have to write a single line of code to spawn scenes. Currently in Godot, if you'd like to implement some kind of scene spawner, you have to create an entire script with export(PackedScene) var scene
exposed, or you'd have to preload("res://enemy.tscn")
, yet hardcoding scene paths inside scripts is not always handy in cases where you move scripts around during refactor, leading to broken scripts.
So even if you think that it's not useful in real conditions, this is certainly useful for prototyping and even (stress)-testing purposes, taking into account that Godot is used a lot for prototyping.
That said, if it seems like the current implementation is not sufficient for variety of use cases, I invite and recommend trying it out (see build artifacts):
If not, please tell me what kind of concrete use cases you typically need to solve, because there will always be specific use cases for every feature in Godot, whatever the implementation.
this function is very simple
It took me 3 days to come up with a decent implementation with all corner cases to account that you'd need to solve via script in real conditions anyways: 300 lines of code! 🙂
I have to agree that as is, it's a very simplistic design. I'm not sure I would make much use of it. It seems like something that would be nice for beginners to get a quick result, or stress testing / prototyping, but not much beyond that. And that would certainly be better than nothing. But I think maybe we can do better.
Almost anytime I'm spawning anything, I use some randomness to make things feel less artificial. Like, every random(1.5, 2) seconds spawn between 2 and 4 objects. Or something similar.
Another thing I often use is a random bag (i.e. shuffled array) that I fill what whatever. It's a simple way to randomly spawn, for example, different enemies, or shapes in Tetris, while keeping the game fair. (This could perhaps be a new datatype by itself.) But on that note, I feel that having some way to spawn multiple types of objects is important to make this really useful.
I think it would be good to have a built-in something that people can then expand on if they need to (e.g. via signals, IIRC this sends a signal whenever it spawns something)
I'm wary to add randomization features directly in C++ because it's something that might be indeed specific and the features may go out of control (you may want to use randf()
, or randfn()
etc). But if you've already used Goost, then Random/Random2D
singletons do provide useful functions to do this:
# Pick a random point in circle and spawn an enemy.
$Spawner2D.position = Random2D.point_in_circle() * radius
$Spawner.spawn() # You don't have to call this manually, only when `enabled = false`.
Another thing I often use is a random bag (i.e. shuffled array) that I fill what whatever.
Similarly, you can choose a random scene:
# Pick a random scene from an array and spawn it.
$Spawner2D.resource = Random.choice(scenes)
$Spawner2D.spawn()
You could use a Timer
to randomize any property of Spawner2D
, or connect to Spawner2D.node_added
signal and override those properties on each spawn:
func _on_node_spawned(node):
$Spawner2D.step = rand_range(1.0, 3.0) # Randomize time step for the next spawn.
So, perhaps in order for this feature to be truly useful, there are two choices:
Random2D
that actually facilitate what's proposed here.But on that note, I feel that having some way to spawn multiple types of objects is important to make this really useful.
This is why I've mentioned about the use case of spawn sequences in goostengine/goost#165, but not sure about implementation. In any case, currently you can either:
Random.choice()
to pick a random scene.Spawner2D
with different scenes, randomizing time step.spawn between 2 and 4 objects
Currently Upon further thinking this won't really work well, because Spawner2D
can only spawn 1 object at a time. Perhaps we could add a property for this, like quantity
(would probably need to rename the Quantity
group to Amount
in the inspector), or vice versa...spawn()
will return a single node, so:
for i in randi_range(2, 4):
$Spawner2D.spawn()
In any case, Godot development does prefer the approach of "do one thing and do it right", or "single responsibility principle", and the current implementation reflects that.
class_name Launcher2D extends Spawner2D export(Vector2) var direction export(float) var power func _spawn(object): if object is RigidBody2D: object.apply_central_impulse(direction * power)
I don't understand, seems like you're not really overriding the spawn function. Was "object" already spawned here? If that's the case couldn't this code just go in the signal callback?
Yes, the development is moving fast, I no longer think the virtual callback is needed, connecting to the node_spawned
signal is enough (I proposed adding the callback prior to adding the signal).
for i in randi_range(2, 4): $Spawner2D.spawn()
But wouldn't I still have to manage my own timer that has this code as a callback? so basically might as well not use the spawner at all? Or, I guess you mentioned the timer could be exposed, but then I'd just be using it as a Timer, essentially.
I dunno... if I have to micro-manage the spawner by explicitly setting the step / resource / position even for fairly simple things like this, then it kinda feels more like a burden than something that is useful and extendable.
But wouldn't I still have to manage my own timer that has this code as a callback?
Not necessarily. You can still connect to node_spawned
signal. At the very least, you need to spawn at least one node, otherwise there's no need to use Spawner2D
in the first place, right?
Then, whenever a new node is spawned, you handle it via _on_node_spawned(node)
method previously connected. You can then decide to spawn a new set of N number of nodes, minus one (if this matters at all):
func _on_node_spawned(node):
for i in randi_range(2, 4) - 1:
var n = $Spawner2D.spawn()
n.position += Random2D.point_in_circle() * radius
With the above snippet, the first spawned node
will remain at the origin of Spawner2D
, while other nodes will spread out. Of course, this is just an example, you can do whatever you want with those nodes, including using other Spawner2D
nodes to spawn nodes from different scenes.
For what it's worth, Spawner2D/3D
is a node which is an enhanced/specialized version of Timer
. You'd need to do extra work if you choose to use Timer
node alone. The only thing which is not implemented in Spawner2D
currently is pause()
method, but could be easily added (though I've rarely if ever had to use Timer.pause()
to be honest).
I dunno... if I have to micro-manage the spawner by explicitly setting the step / resource / position even for fairly simple things like this, then it kinda feels more like a burden than something that is useful and extendable.
I think what you really ask for are those randomization features, what I proposed so far is core functionality.
I'm thinking that perhaps we need to go with Particles2D/3D
approach and add SpawnMaterial
class, that way we could implement all those rand features freely without fearing the API bloat of the main class.
I'm thinking that perhaps we need to go with
Particles2D/3D
approach and addSpawnMaterial
class
I'd definitely be curious to see what that would look like :)
func _on_node_spawned(node): for i in randi_range(2, 4) - 1: var n = $Spawner2D.spawn() n.position += Random2D.point_in_circle() * radius
Would this not cause an infinite loop of spawning?
Would this not cause an infinite loop of spawning?
Yeah, I've been thinking whether force_spawn()
is needed. But this won't cause infinite loop if you spawn from different Spawner2D
which is not connected to that signal, since you asked for ability to spawn different objects at the same time.
There's' still a way to workaround this by calling Object.set_block_signals()
:
func _on_node_spawned(node):
$Spawner2D.set_block_signals(true)
var n = $Spawner2D.spawn()
$Spawner2D.set_block_signals(false)
I realize that this is micro-managing, but we're discussing whether current functionality is sufficient to fulfill those needs to justify adding more complexity to the core class.
(Update: I have changed the behavior now, calling spawn()
manually will no longer emit node_spawned
signal, the signal will only be emitted per time step if enabled = true
).
Note that I'm not against adding randomization features, and that was actually my original intent (for instance, see "Region" section in this proposal). But unlike Particles2D/3D
, the Spawner2D/3D
will be used for gameplay logic for 90-95% of use cases, and only 5-10% for effects. Therefore, anything related to randomness must be dealt with care.
So far I see two options:
SpawnerMaterial
and optionally randomize everything.Spawner2D/3D
as RandomSpawner2D/3D
.The SpawnerMaterial
is likely superior because you could reuse it (Resource
) between scenes/nodes.
But I'd like to say that the current feature set is fine in and of itself (according to my needs, at least), especially when there's already a way to randomize properties using RandomNumberGenerator
. Note that it's also possible to spawn everything deterministically if you use your own instance of RandomNumberGenerator
, which is important to some people (including myself: godotengine/godot#45019).
It's also a question of whether this feature will end up implemented in Godot or in Goost. If latter, then I can also take advantage of existing Goost classes like PolyNode2D
and family (in order to conveniently define spawn regions via boolean operations, like spawning inside a "donut" region, for instance see use case in this proposal: #199).
It's not really a matter of whether you're beginner or not either: anything that speeds up existing workflow is an improvement in my eyes. Of course when I say this, I'm mostly talking from the standpoint of Goost development principles, which might not be in alignment with Godot ones.
So, back to design now!.. I have merged goostengine/goost#165 to let people play with the core implementation. Note that you can download latest nightly editor builds from https://github.com/goostengine/goost#building-. See also example project I've used at https://github.com/goostengine/goost-examples/tree/gd3/2d/spawner.
Describe the project you are working on
Goost - Godot Engine Extension
Describe the problem or limitation you are having in your project
I often find myself in the need to spawn various nodes/objects in the scene. The problem is that it's not as convenient as it could be, especially when more advanced spawning options are needed.
Note that this proposal is completely different to #3359, because this proposal solves completely different set of use cases.
Describe the feature / enhancement and how it helps to overcome the problem or limitation
I propose adding
Spawner2D
andSpawner3D
nodes to facilitate the common need to spawn objects in the scene. Note that some of this functionality can overlap with particles nodes in Godot, this makes sense to me and I think what I propose below would be an intuitive API.Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
Core
A general-purpose
Spawner2D/3D
node should have ability to set any kind of object to be used as a blueprint for spawning. For instance,Spawner.object
property could accept:PackedScene
Script
Object/Node
itself (the spawner would produce a duplicate of an existing instance).Add
spawn()
method that will instantiate anobject
as a direct child ofSpawner
. Perhapsnode_path
could be exposed to make this more flexible.See also https://github.com/godotengine/godot-proposals/issues/3651#issuecomment-991614188.
Spawning behavior and timing
Timer functionality would definitely be essential to have as well. Spawning could be manual (by calling method
spawn()
above), or automatic/repeated.spawning
property that will disable or enable continuous spawning.spawn_rate
.spawning = true
, then you could just callspawn()
manually right after.amount
property (spawn just the right amount of objects, then switch tospawning = false
).lifeteme
property, measured in seconds (delete objects automatically whenlifetime
reaches zero).Adding a
spawned
signal would be needed for automatic spawning (which is not always a direct result of callingspawn()
).For more advanced use cases, perhaps just exposing
Timer
node withSpawner.get_timer()
would be enough, but it should be used with caution (godotengine/godot#30391 would be useful here).But I think that timer functionality should be an implementation detail, because it would also be possible to use another external
Timer
that would create spawn waves. At the same time, it's not even required to use anotherTimer
to achieve this, because you could use aSpawner
node to spawn moreSpawner
nodes. 😉See also https://github.com/godotengine/godot-proposals/issues/3651#issuecomment-991640984.
Overriding callbacks
A
_spawn()
virtual method could be added for scripts to override. For instance, you could make aSpawner
launch projectiles:Region
By default, a
Spawner
would only instantiate nodes at origin. You'd think that it's enough, but there are more advanced use cases that are difficult to solve:I realize that spawning inside something randomly asks for adding
rand
methods inside the class, which should likely be avoided as this stuff could be done via script. However, we could still provide some sort ofSpawnShape
enum/property for common shapes (similarly toemission_shape
inParticlesMaterial
), for use cases where determinism is not needed and does not affect gameplay logic (for instance, particle effects that react to collisions).The
Spawner
node's position/offset could be animated in various ways, also mimicking the spawn region that way.Usage
The way it would work:
Spawner
node anywhere in the scene tree.object
property of that node.Related
1344
If this enhancement will not be used often, can it be worked around with a few lines of script?
It will be used often:
Is there a reason why this should be core and not an add-on in the asset library?
This could be in core because even more lightweight game engines like Orx has this, see for instance this API: https://github.com/orx/orx/blob/5c01d089fc452ee8d78e3a238b73775769091f2e/code/include/object/orxSpawner.h
I'd like to implement what I proposed above in Goost, regardless of whether this proposal is approved to be implemented in Godot.
Feedback welcome! I'm interested in what kind of functionality you'd find essential and useful.