This tutorial is showing how to use spawners.
NB: If you want to see how to use orx while using C++
for your game, please refer to the localization tutorial.
See previous basic tutorials for more info about basic object creation, clock handling, frames hierarchy, animations, cameras & viewports, sounds & musics, FXs, physics, scrolling and C++ localization.
This tutorial shows how to create and use spawners for particle effects.
It's only a tiny possibility of what can be achieved using them.
For example, they can be also used for generating monsters or firing bullets, etc…
The code here is only used for two tasks:
Beside that, this tutorial is completely data-driven: the different test settings and the input definitions are stored in the config files along all the spawning/move/display logic.
With this very small amount of lines of code, you can have an infinite number of results: playing with physics, additive/multiply blend, masking, speed/acceleration of objects, etc…
It's all up to you! All you need is changing the config files and you can even test your changes without restarting this tutorial: config files will be entirely reloaded when switching from a test to another.
If there are too many particles displayed for your config, just turn down the amount of particles spawned per wave and/or the frequency of the waves.
To do so, search for the WaveSize
/WaveDelay
attributes in the different spawner sections. Have fun!
Let's begin with a quick look to our main
function.
int main(int argc, char **argv) { orx_Execute(argc, argv, Init, Run, Exit); return EXIT_SUCCESS; }
Nothing new here, we only execute orx using the helper orx_Execute
function, providing three callbacks: Init()
, Run()
and Exit()
.
Let's now have a glimpse to our Init()
function.
orxSTATUS orxFASTCALL Init() { return LoadConfig(); }
We simply call our LoadConfig()
function that will be described below.
void orxFASTCALL Exit() { }
As you can see, our Exit()
function is even shorter as we will let orx the task of cleaning everything when we quit.
orxSTATUS orxFASTCALL Run() { orxSTATUS eResult = orxSTATUS_SUCCESS; if(orxInput_IsActive("NextConfig") && orxInput_HasNewStatus("NextConfig")) { ss32ConfigID = (ss32ConfigID < orxConfig_GetListCounter("ConfigIDList") - 1) ? ss32ConfigID + 1 : 0; LoadConfig(); } else if(orxInput_IsActive("PreviousConfig") && orxInput_HasNewStatus("PreviousConfig")) { ss32ConfigID = (ss32ConfigID > 0) ? ss32ConfigID - 1 : orxConfig_GetListCounter("ConfigIDList") - 1; LoadConfig(); } else if(orxInput_IsActive("Quit")) { eResult = orxSTATUS_FAILURE; } return eResult; }
In our Run()
function, in addition to quitting when the input Quit
is active, we update our global ss32ConfigID
which is an index to our currently selected config, depending on our inputs PreviousConfig
& NextConfig
status.
They'll then both call LoadConfig()
that will reload everything.
Let's now have a closer look to LoadConfig()
.
static orxINLINE orxSTATUS LoadConfig() { orxSTATUS eResult = orxSTATUS_FAILURE; if(pstScene) { orxObject_Delete(pstScene); pstScene = orxNULL; } if(pstViewport) { orxViewport_Delete(pstViewport); pstViewport = orxNULL; } orxConfig_Clear(); orxConfig_Load(orxConfig_GetMainFileName()); orxConfig_SelectSection("Tutorial"); if(ss32ConfigID < orxConfig_GetListCounter("ConfigList")) { orxSTRING zConfigFile; zConfigFile = orxConfig_GetListString("ConfigList", ss32ConfigID); if((eResult = orxConfig_Load(zConfigFile)) != orxSTATUS_FAILURE) { pstViewport = orxViewport_CreateFromConfig("Viewport"); pstScene = orxObject_CreateFromConfig("Scene"); } } return eResult; }
As you can see we first delete our Viewport
and our Scene
object, if needed.
We then clean the whole config content and reload our main config file 1).
We then try to find the current config settings we want from the ConfigList
.
If we've found it, we then load the corresponding file.
Some config files will add new properties, some will override basic ones (like the Viewport
background color or the content of the Scene
object).
Finally we (re-)create our Viewport
and our Scene
object.
And we're done for the code! =D
Again, nothing really specific here to spawners or particle handling: all the magic will come from the config files.
In the same way we did for the C++ localization tutorial, the config file in our subfolder 2) merely includes the config file from the parent folder.
We'll then skip this one and directly have a look to the parent folder's one.
As you can see by looking at the config file, we provide values for all basics system (Display
, Physics
, Input
, Viewport
, etc…).
As we've already covered all this in previous tutorials, we won't do it here again.
However, there's an additional section called Tutorial
. Let's have a closer look.
[Tutorial] ConfigList = ../11_Base.ini#../11_Blend.ini#../11_Mask.ini#../11_Physics.ini#../11_BlendMask.ini#../11_BlendPhysics.ini#../11_FusionFountain.ini#../11_MeltingDots.ini#../11_Shader.ini#../11_BlendShader.ini
If you remember, in our LoadConfig()
function, we were selecting the Tutorial
section 3) to look at the property called ConfigList
.
This ConfigList
provides our program with a list of config files, one file for each particle settings.
We then loaded the corresponding file manually by calling orxConfig_Load()
.
NB: All paths defined in config files are relative to the current directory at runtime. By default, it's the executable's one, hence the use of “../
” in front of all our files.
Let's now look at some of them 4).
We begin with the most basic one: 11_Base.ini
.
@../11_ParticleBase.ini@ [Tutorial] Name = Base
The first line tells orx to include ../11_ParticleBase.ini
which will be instantly loaded and processed.
We then extend our Tutorial
section by adding a property called Name
that contains the value Base
.
As we'll see later, we'll use this property to display on screen the current setting's name.
Let's see another one, such as 11_FusionFountain.ini
.
@../11_ParticleBase.ini@ @../11_ParticleBlend.ini@ @../11_ParticleMask.ini@ @../11_ParticleDot.ini@ [Tutorial] Name = FusionFountain
As we can see, it's built the same way as 11_Base.ini
, except that we now load 4 config files instead of a single one and that we provide a different name for Tutorial.Name
.
NB: The file include order is important as the last included file might override properties defined in the first included one, which will happen in our case as we'll see later.
Let's now see 11_ParticleBase.ini
which is included in all our settings.
We'll only cover the interesting details and skipp all the fx/particle configuration.
[Scene] ChildList = ParticleSpawner
Now we know exactly the content of our Scene
object!
Let's see what this ParticleSpawner
is made of.
[ParticleSpawner] ChildList = Name Spawner = Spawner Position = (0, 200, 0)
Another child here! This one's called Name
.
There's also an attribute called Spawner
which is defined.
Let's have a very quick glance at the Name
object.
[Name] Graphic = NameGraphic Position = (0, 50, 0) [NameGraphic] Pivot = Center Text = NameText [NameText] String = @Tutorial.Name
As we can see, Name
is simply a text object, however it's content points to Tutorial.Name
.
As we just saw, Tutorial.Name
is different for each config setting.
This means that every time we create a ParticleSpawner
object, we'll display the name of the current setting just next to it, thanks to the relative positioning in a parent/child relation.
Let's get back to our Spawner
.
[Spawner] Object = Particle WaveSize = 5 WaveDelay = 0.01
Spawners can use far more attributes than the three defined above as we can see in https://github.com/orx/orx/blob/master/tutorial/bin/CreationTemplate.ini, but those are the most important ones.
First, the Object
attribute tells the spawner which kind of object it's going to spawn. In this case it's an object which is called Particle
and that we won't describe here 5).
We can also learn from this spawner that it will spawn 5 of these Particle
every 0.01 seconds by looking at the WaveSize
and WaveDelay
attributes.
So that's about it, our Spawner
will spew 500 Particle
every second as long as it's active.
Gladly the Particle
object has a LifeTime
attribute of 2.0 seconds which means we won't have more than 1000 Particle
existing at the same time.
There are a lot of other attributes to have a better control over our Spawner
, such as limiting the total number of object spawned, or the maximum number of existing objects at the same time, etc…
As we saw for the FusionFountain
setting, we also load other config files.
Let's then have a look to them.
We'll begin with 11_ParticleBlend.ini
[ParticleGraphic] Texture = ../../data/object/particle2.png [FadeOut] Type = color StartValue = (0, 0, 0) ~ (-50, -50, -50) EndValue = (-255, -255, -255) [Particle] Graphic = ParticleGraphic BlendMode = add Color = (255, 255, 0) # (0, 255, 0) # (255, 0, 255) # (0, 255, 255) Position = (-40, 0, 0) ~ (40, 0, 0) [Spawner] WaveSize = 8 [ParticleFX] SlotList = FadeOut#Gravity
All those defined properties are actually overriding the ones defined in 11_ParticleBase.ini
.
We can see that we want to spawn more Particle
objects in every Spawner
's wave 6).
We can also see that we change the Texture
, the BlendMode
and even the FX of our Particle
, etc…
Let's now look at 11_ParticleMask.ini
[Scene] ChildList = ParticleSpawner # Mask [MaskGraphic] Texture = ../../data/object/mask.png Pivot = center BlendMode = multiply [Mask] Graphic = MaskGraphic Position = {0, 0, -0.1} [Particle] Color = (255, 255, 255)
The most important information we can see here is that our Scene
object now has two children instead of one.
This means that, in addition to our ParticleSpawner
object, we also create an object called Mask
which will use a multiply
BlendMode
and will displayed on top of the spawned Particle
objects 7).
We also change the color of our Particle
so that it will always be white.
Finally, let's see 11_ParticleDot.ini
.
First we can see that we change our Viewport
background color.
[Viewport] BackgroundColor = (200, 200, 200)
As it's now a light grey, we also change the Name
color to black.
[Name] Color = (0, 0, 0)
And we make sure we spawn only 5 Particle
objects per wave.
[Spawner] WaveSize = 5
We also add a child to our Particle
[Particle] ChildList = ParticleDot
That means that every Particle
will have its own ParticleDot
child that will follow it everywhere it goes! No more lonely Particle
!
Let's have a closer look to this ParticleDot
.
[ParticleDotGraphic] Texture = ../../data/object/particle.png Pivot = center [ParticleDot] Graphic = ParticleDotGraphic Position = (0, 0, -0.001) Alpha = 1.0 Scale = 0.9
We can see that this dot will be placed in front of each Particle
8) and that it will be a bit smaller than Particle
as a relative scale of 0.9.
As they both use the same Texture
, even if we scale Particle
, ParticleDot
will always have 0.9 times the size of its parent Particle
.
That's it for particles, let's now have a quick look to 11_ParticleShader.ini
.
[Viewport] ShaderList = Decompose
Here we add a shader called Colorize
to our Viewport
.
[Decompose] Code = "void main() { float fRed, fGreen, fBlue; // Computes positions with offsets vec2 vRedPos = vec2(gl_TexCoord[0].x + offset.x, gl_TexCoord[0].y + offset.y); vec2 vGreenPos = vec2(gl_TexCoord[0].x, gl_TexCoord[0].y); vec2 vBluePos = vec2(gl_TexCoord[0].x - offset.x, gl_TexCoord[0].y - offset.y); // Red pixel inside texture? if((vRedPos.x >= 0.0) && (vRedPos.x <= 1.0) && (vRedPos.y >= 0.0) && (vRedPos.y <= 1.0)) { // Gets its value fRed = texture2D(texture, vRedPos).r; } // Green pixel inside texture? if((vGreenPos.x >= 0.0) && (vGreenPos.x <= 1.0) && (vGreenPos.y >= 0.0) && (vGreenPos.y <= 1.0)) { // Gets its value fGreen = texture2D(texture, vGreenPos).g; } // Blue pixel inside texture? if((vBluePos.x >= 0.0) && (vBluePos.x <= 1.0) && (vBluePos.y >= 0.0) && (vBluePos.y <= 1.0)) { // Gets its value fBlue = texture2D(texture, vBluePos).b; } // Outputs the final decomposed pixel gl_FragColor = vec4(fRed, fGreen, fBlue, 1.0); }" ParamList = texture#offset offset = (-0.05, -0.05, 0.0) ~ (0.05, 0.05, 0.0); <= Let's take some random offset
As you can see, we directly write our shader's code in the Code
attribute.
In our case it's a trivial code that will decompose the color of a pixel into 3 channels and mix them with its neighbors.
After defining the code 9), we provide a parameter list called ParamList
.
Here we define 2 parameters: texture
and offset
.
As you can see we give random values for the offset
parameter. The random values will be picked when the shader Decompose
is created.
We can also see that we didn't define our texture
paremeter. This means its content will default to the current parent's texture: here it's our Viewport
texture 10).
In our case all the parameters are code-independent, ie. they won't change at runtime.
However we can add runtime parameters that we can change on-the-fly, but this won't be covered in detail by this tutorial.
In order to do so, we need to add the attribute UseCustomParam
with a value set to true
.
If we do that, every time our shader will be executed, orx will send an orxEVENT_TYPE_SHADER
event 11).
This event's payload will contain the parameter name 12), its type and its config default value.
You can then change its value on-the-fly to suit your needs.
There's a simple sine wave distortion example that you can see in the bounce demo which is included in orx's source package, source and config.
Source code: 11_Spawner.c
Config file: 11_Spawner.ini
orxConfig_SelectSection()
Particles
are spawned with a default Z=0.0 whereas Mask
has a Z=-0.1, which is closer to the cameratexture
will be the object's texture insteadeID == orxSHADER_EVENT_SET_PARAM
texture
or offset