Your First Reactor¶
In this tutorial, you'll build a complete NUClear application from scratch. By the end, you'll have a working program that starts up, emits a message, and reacts to it — covering the core concepts you'll use in every NUClear project.
What You'll Build¶
A small application with a single reactor that:
- Logs a greeting when the system starts
- Emits a custom message
- Reacts to that message and logs its contents
It's simple, but it demonstrates the fundamental pattern behind every NUClear system: reactors emit messages, and other reactions respond to them.
Project Setup¶
Create a new directory for your project with the following structure:
CMakeLists.txt¶
cmake_minimum_required(VERSION 3.15)
project(hello_nuclear LANGUAGES CXX)
find_package(NUClear REQUIRED)
add_executable(hello_nuclear
src/main.cpp
)
target_link_libraries(hello_nuclear PRIVATE NUClear::nuclear)
target_compile_features(hello_nuclear PUBLIC cxx_std_14)
Finding NUClear
If not, head back to the Installation tutorial first.
Creating Your Reactor Class¶
A reactor is a class that groups related reactions together. Think of it as a component in your system — it has its own state, its own reactions, and communicates with other reactors through messages.
Every reactor must:
- Inherit from
NUClear::Reactor - Accept a
std::unique_ptr<NUClear::Environment>in its constructor - Pass that environment to the base class
Here's the skeleton:
#ifndef HELLO_REACTOR_HPP
#define HELLO_REACTOR_HPP
#include <nuclear>
class HelloReactor : public NUClear::Reactor {
public:
explicit HelloReactor(std::unique_ptr<NUClear::Environment> environment)
: Reactor(std::move(environment)) {
// Reactions are registered here
}
};
#endif // HELLO_REACTOR_HPP
classDiagram
class NUClear_Reactor {
+PowerPlant& powerplant
+on~DSL...~(args...) Binder
+emit(std::unique_ptr~T~)
+log~level~(args...)
}
class HelloReactor {
+HelloReactor(std::unique_ptr~Environment~)
}
NUClear_Reactor <|-- HelloReactor : inherits
Why a unique_ptr<Environment>?
The environment carries metadata about the reactor (like its name) and a reference to the PowerPlant it lives in.
Passing it as a unique_ptr gives NUClear ownership semantics — each reactor gets its own environment, and the framework manages the lifecycle.
Adding a Startup Reaction¶
The simplest reaction you can write responds to Startup — a built-in event that fires once when the PowerPlant begins running.
Add this inside the constructor:
Let's break this down:
on<Startup>()— Declares that this reaction should trigger on theStartupevent.then(...)— Provides the callback to execute when the event fireslog<INFO>(...)— Logs a message at theINFOlevel
The on<...>().then(...) pattern is the core API for creating reactions.
The template parameters (the DSL words) describe when the reaction runs, and the .then() callback describes what it does.
Emitting a Message¶
Messages in NUClear are just plain C++ structs or classes. There's no special base class, no registration macro — any type can be a message.
Define a simple message struct inside your reactor:
Now emit it from your Startup reaction:
on<Startup>().then([this]() {
log<INFO>("HelloReactor has started!");
emit(std::make_unique<Greeting>(Greeting{"Hello from NUClear!"}));
});
Messages are emitted as std::unique_ptr
This gives NUClear clear ownership of the data and enables efficient, thread-safe message passing without copying.
Reacting to the Message¶
Now let's add a reaction that triggers whenever a Greeting message is emitted.
The Trigger<T> DSL word binds a reaction to emissions of type T:
on<Trigger<Greeting>>().then([this](const Greeting& greeting) {
log<INFO>("Received:", greeting.message);
powerplant.shutdown();
});
When a Greeting is emitted anywhere in the system, this callback fires and receives a const reference to the message data.
After logging it, we call powerplant.shutdown() to stop the system cleanly.
Lambda parameters
When you use Trigger<Greeting>, you can accept const Greeting& as a parameter to access the message contents.
Putting It All Together¶
HelloReactor.hpp¶
#ifndef HELLO_REACTOR_HPP
#define HELLO_REACTOR_HPP
#include <memory>
#include <string>
#include <utility>
#include <nuclear>
class HelloReactor : public NUClear::Reactor {
public:
/// A simple message carrying a greeting string
struct Greeting {
std::string message;
};
explicit HelloReactor(std::unique_ptr<NUClear::Environment> environment)
: Reactor(std::move(environment)) {
// React to Greeting messages
on<Trigger<Greeting>>().then([this](const Greeting& greeting) {
log<INFO>("Received:", greeting.message);
powerplant.shutdown();
});
// Run once at startup
on<Startup>().then([this]() {
log<INFO>("HelloReactor has started!");
emit(std::make_unique<Greeting>(Greeting{"Hello from NUClear!"}));
});
}
};
#endif // HELLO_REACTOR_HPP
main.cpp¶
#include <nuclear>
#include "HelloReactor.hpp"
int main(int argc, const char* argv[]) {
// Configure the PowerPlant
NUClear::Configuration config;
config.default_pool_concurrency = 1;
// Create the PowerPlant
NUClear::PowerPlant plant(config, argc, argv);
// Install our reactor
plant.install<HelloReactor>();
// Start the system (blocks until shutdown)
plant.start();
return 0;
}
Why default_pool_concurrency = 1?
In real applications, you'd typically leave this at the default (the number of hardware threads) or tune it for your workload.
Building and Running¶
Expected output:
What's Happening Under the Hood¶
Here's the sequence of events when you run your program:
sequenceDiagram
participant Main as main()
participant PP as PowerPlant
participant HR as HelloReactor
Main->>PP: PowerPlant(config)
Main->>PP: install<HelloReactor>()
PP->>HR: Construct reactor
HR->>HR: Register reactions
Main->>PP: start()
PP->>HR: Startup event
HR->>HR: log("HelloReactor has started!")
HR->>PP: emit(Greeting)
PP->>HR: Trigger<Greeting> fires
HR->>HR: log("Received:", message)
HR->>PP: shutdown()
PP->>Main: start() returns
The key insight is that the constructor only registers reactions — it doesn't execute them.
Reactions only fire after plant.start() is called.
The Startup event is emitted automatically by the PowerPlant, which triggers your first callback, which emits a Greeting, which triggers your second callback.
This is the reactive pattern: you declare what should happen in response to what events, and the framework handles the scheduling, threading, and data delivery.
Order of registration
Notice that we registered the `Trigger<Greeting>` reaction *before* the `Startup` reaction in the constructor.
The order of registration doesn't matter for triggering — what matters is when the data is emitted.
The Greeting reaction is ready and waiting by the time Startup fires and emits the message.
Next Steps¶
You've built your first reactor and seen the core pattern: register reactions in the constructor, emit messages to communicate, and let the framework handle the rest.
From here, you can explore:
- Message Passing — Learn how multiple reactors communicate through shared message types
- Periodic Tasks — Set up timers and recurring work with
Every