particle_systems_tutorial - ryzom/ryzomcore GitHub Wiki


title: Particle systems description: Load and control particle system effects in a scene published: true date: 2026-03-14T00:00:00.000Z tags: editor: markdown dateCreated: 2026-03-14T00:00:00.000Z

In the previous tutorial we played animations on a skeleton. Now we'll add visual effects by loading and controlling particle system files (.ps) in the scene.

This tutorial covers:

  • Loading particle system files with createInstance
  • Positioning and scaling particle effects
  • Starting and stopping effects
  • Attaching effects to skeletons

Loading a particle system

Particle system files (.ps) are loaded exactly like shape files, using UScene::createInstance():

	NL3D::UInstance particles = m_Scene->createInstance("snow.ps");

The returned UInstance can be positioned, scaled, and shown/hidden like any other instance:

	particles.setPos(NLMISC::CVector(0.f, 0.f, 0.f));

Particle systems are created and edited using the Object Viewer particle editor. The .ps file contains all the emitters, forces, and particle types that make up the effect.

Starting and stopping

Some particle systems have start/stop capability (one-shot effects like explosions). Check with canStartStop():

	if (particles.canStartStop())
	{
		particles.start();
	}

You can later stop and restart:

	particles.stop();
	// ...
	particles.start();

Use isStarted() to query the current state.

Continuous effects (like snow or fire) typically start automatically when created and run until deleted.

Scaling effects

Particle systems respond to the instance's scale. This is useful for making bigger or smaller versions of the same effect:

	particles.setScale(NLMISC::CVector(2.f, 2.f, 2.f));

Advanced: UParticleSystemInstance

For particle-specific control beyond start/stop, cast the UInstance to UParticleSystemInstance. This gives access to user parameters, manual emission, and color modulation:

#include <nel/3d/u_particle_system_instance.h>
	NL3D::UParticleSystemInstance psFX;
	psFX.cast(particles);
	if (!psFX.empty())
	{
		// User parameters (0-3) control artist-defined behaviors, range 0.0 to 1.0
		psFX.setUserParam(0, 0.5f);

		// Modulate the color of the entire system
		psFX.setUserColor(NLMISC::CRGBA(255, 200, 150));

		// Toggle all emitters on/off
		psFX.activateEmitters(true);

		// Force the system to allocate resources even when off-screen
		// (important for gameplay-critical effects like spells)
		psFX.forceInstanciate();
	}

User parameters are values from 0 to 1 that the particle system artist can wire to any property (emission rate, size, speed, color, etc.) when designing the effect in the Object Viewer.

Attaching to a skeleton

To attach a particle effect to a bone on a skeleton (for example, a magic spell on a character's hand), use USkeleton::stickObject():

	sint boneId = m_Skeleton.getBoneIdByName("Bip01 R Hand");
	if (boneId >= 0)
	{
		m_Skeleton.stickObject(particles, (uint)boneId);
	}

The particle system will then follow the bone's position and orientation. To detach:

	m_Skeleton.detachSkeletonSon(particles);

Cleaning up

Delete particle instances like any other instance:

	m_Scene->deleteInstance(particles);

Complete source code

This example creates a scene with a shape and two particle effects: one attached to the scene at a fixed position, and another that can be toggled with the space bar. Since particle system files are created with the Object Viewer tool, this tutorial uses placeholder filenames that you'll need to replace with actual .ps files from the Snowballs data or your own creations.

#include <nel/misc/types_nl.h>
#include <nel/misc/app_context.h>
#include <nel/misc/event_listener.h>
#include <nel/misc/debug.h>
#include <nel/misc/time_nl.h>
#include <nel/misc/path.h>
#include <nel/3d/u_driver.h>
#include <nel/3d/u_scene.h>
#include <nel/3d/u_camera.h>
#include <nel/3d/u_instance.h>
#include <nel/3d/u_text_context.h>

using namespace std;
using namespace NLMISC;

class CMyGame : public IEventListener
{
public:
	CMyGame();
	~CMyGame();
	void run();
	virtual void operator()(const CEvent &event) NL_OVERRIDE;

private:
	NL3D::UDriver *m_Driver;
	NL3D::UScene *m_Scene;
	NL3D::UTextContext *m_TextContext;
	NL3D::UInstance m_Entity;
	NL3D::UInstance m_AmbientPS;
	NL3D::UInstance m_EffectPS;
	bool m_EffectActive;
	bool m_CloseWindow;
	double m_LastTime;
	float m_CamAngle;
};

CMyGame::CMyGame()
	: m_CloseWindow(false)
	, m_Scene(NULL)
	, m_TextContext(NULL)
	, m_EffectActive(false)
	, m_CamAngle(0.f)
{
	m_Driver = NL3D::UDriver::createDriver(0, NL3D::UDriver::OpenGl3);
	if (!m_Driver) { nlerror("Failed to create driver"); return; }

	m_Driver->EventServer.addListener(EventCloseWindowId, this);
	m_Driver->EventServer.addListener(EventKeyDownId, this);
	m_Driver->setDisplay(NL3D::UDriver::CMode(800, 600, 32));
	m_Driver->setWindowTitle("Particle Systems");

	CPath::addSearchPath("data", true, false);
	CPath::remapExtension("dds", "tga", true);

	// Optional text context for HUD
	m_TextContext = m_Driver->createTextContext("n019003l.pfb");
	if (m_TextContext) m_TextContext->setFontSize(12);

	// Create scene with sun lighting
	m_Scene = m_Driver->createScene(true);
	m_Scene->enableLightingSystem(true);
	m_Scene->setSunDiffuse(CRGBA(255, 255, 240));
	m_Scene->setSunDirection(CVector(1.f, 1.f, -2.f).normed());

	// Camera
	NL3D::UCamera cam = m_Scene->getCam();
	cam.setTransformMode(NL3D::UTransformable::DirectMatrix);
	cam.setPerspective(float(Pi / 3.0), 800.f / 600.f, 0.1f, 1000.f);

	// Load a shape for context
	m_Entity = m_Scene->createInstance("box02.shape");
	if (!m_Entity.empty())
		m_Entity.setPos(CVector(0.f, 0.f, 0.f));

	// Load an ambient particle system (continuous, like snow)
	// Replace with an actual .ps filename from your data
	m_AmbientPS = m_Scene->createInstance("snow.ps");
	if (!m_AmbientPS.empty())
		m_AmbientPS.setPos(CVector(0.f, 0.f, 3.f));

	// Load a one-shot effect particle system
	// Replace with an actual .ps filename from your data
	m_EffectPS = m_Scene->createInstance("appear.ps");
	if (!m_EffectPS.empty())
	{
		m_EffectPS.setPos(CVector(0.f, 0.f, 1.f));
		// One-shot effects may need explicit start
		if (m_EffectPS.canStartStop())
			m_EffectPS.stop();
	}

	m_LastTime = CTime::ticksToSecond(CTime::getPerformanceTime());
}

CMyGame::~CMyGame()
{
	if (!m_EffectPS.empty()) m_Scene->deleteInstance(m_EffectPS);
	if (!m_AmbientPS.empty()) m_Scene->deleteInstance(m_AmbientPS);
	if (!m_Entity.empty()) m_Scene->deleteInstance(m_Entity);
	if (m_TextContext) m_Driver->deleteTextContext(m_TextContext);
	if (m_Scene) m_Driver->deleteScene(m_Scene);
	m_Driver->release();
	delete m_Driver;
}

void CMyGame::operator()(const CEvent &event)
{
	if (event == EventCloseWindowId)
		m_CloseWindow = true;
	else if (event == EventKeyDownId)
	{
		CEventKeyDown &kd = (CEventKeyDown &)event;
		if (kd.Key == KeySPACE && kd.FirstTime)
		{
			// Toggle the one-shot effect
			if (!m_EffectPS.empty() && m_EffectPS.canStartStop())
			{
				if (m_EffectActive)
					m_EffectPS.stop();
				else
					m_EffectPS.start();
				m_EffectActive = !m_EffectActive;
			}
		}
	}
}

void CMyGame::run()
{
	while (m_Driver->isActive() && !m_CloseWindow)
	{
		m_Driver->EventServer.pump();

		double now = CTime::ticksToSecond(CTime::getPerformanceTime());
		float dt = float(now - m_LastTime);
		m_LastTime = now;
		m_CamAngle += dt * 0.3f;

		CVector eye(cosf(m_CamAngle) * 6.f, sinf(m_CamAngle) * 6.f, 3.f);
		m_Scene->getCam().lookAt(eye, CVector(0.f, 0.f, 1.f));

		m_Driver->clearBuffers(CRGBA(10, 10, 15));
		m_Scene->animate(CTime::getLocalTime() / 1000.0);
		m_Scene->render();

		// HUD
		if (m_TextContext)
		{
			m_TextContext->setHotSpot(NL3D::UTextContext::TopLeft);
			m_TextContext->setColor(CRGBA::White);
			m_TextContext->printfAt(0.01f, 0.99f,
				"[Space] Effect: %s", m_EffectActive ? "ON" : "OFF");
		}

		m_Driver->swapBuffers();
	}
}

int main(int argc, char *argv[])
{
	CApplicationContext applicationContext;
	CMyGame myGame;
	myGame.run();
	return EXIT_SUCCESS;
}

Place shape and particle system files in a data directory next to the executable. The Snowballs data archive contains snow.ps, appear.ps, disappear.ps, and other particle effects you can use.

What's next

You now know the core 3D rendering features of NeL: primitives, textures, shapes, lights, animations, and particle systems. The next tutorials will cover game infrastructure topics like config files, Georges data sheets, and networking.

⚠️ **GitHub.com Fallback** ⚠️