Tutorial
This page is meant to teach new RoboCup Software members the basics of what they’ll need to contribute. Upon completion of this tutorial, you’ll have the knowledge and trust of your teammates to implement new features on your own.
If you have absolutely no familiarity with ROS or C++, check out the Introduction page first.
No prior experience is assumed. However, a bit of stubbornness is required. RoboCup SW has seen many members without any prior CS experience become valued contributors, and many talented CS majors quit within a few weeks.
Note: you must have Ubuntu 22.04 installed in some capacity to use our stack. You should make sure that you have gone through all the installation steps before proceeding here.
The tutorial is structured as follows.
There are some gaps intentionally left in the tutorial. This is to force you to problem-solve on your own, simulating what it feels like to write a new feature. In other words, unlike the introduction section (if you’ve gone through that), the descriptions for some sections of this tutorial are intentionally left vague and it’s up to you to figure it out!
Make sure you take your time with this. We don’t care if you spend the rest of the semester working on this. Learning this takes time, and learning this well takes even more time. You may also have to read some more about C++ to work on this. Check out Learn CPP - almost all veteran RoboCup members have used this website to learn the basics.
When you run into issues, your order of question-asking should be:
Google
Keywords, not full sentences
Error messages, if they come up
Fellow new members
ChatGPT/Claude/Gemini/Grok/Whatever
Great at helping you out with errors. Don’t expect to be able to vibe code stuff, though.
FAQ page in our docs (common errors and debug info)
Software lead
Anyone the SW lead takes advice from (basically any veteran SW member)
This is not because older members don’t want to help you, but because if older members helped every new member with every question, they wouldn’t have time to make our robots better (nor would you learn as much). So try to resolve your issue yourself, and expect to be asked “what have you tried already?” when you ask for help.
1. GitHub Basics
Now that you have everything installed and understand the basics of the command-line and git, let’s get started using GitHub.
Note
git is a command line version-control tool. GitHub is a website to host shared files, and is well-integrated with git, but is not the same thing.
First, use git to checkout the main branch for our stack, and then pull its latest version:
git checkout ros2 git pull
Next, create a new branch under this naming scheme:
git checkout -b "<your-name>/robocup-sw-tutorial"
For instance, the author’s branch would be named
kevin-fu/robocup-sw-tutorial.
Launch soccer (our UI) and the ER-force simulator, same way as you did in the installation guide. Press the green check mark. You should see three wallers and one goalie move into position. Click anywhere on the field to place the ball in that location. You should see all five robots move between the ball and the goal.
Open the file src/rj_strategy/src/agent/position/waller.cpp.
Find the line of code that calculates the wall_spacing and double its value.
Re-build the project (using the method specified in the installation guide) and run the simulator again. You should see the wallers more spread out. Note that this is probably a less effective wall! This change is just for educational purposes.
Take a screenshot of your new wall.
Now that you’ve made a change to the repo, run git status. You should see
that whatever files you changed show up in red, which indicates that they are
unstaged. Make sure only the file you changed is shown, and not additional files.
Stage the files you changed with git add (Google this if unsure
how, or see the previous section on git), then commit them:
git commit -m '<commit msg>'
Note
<commit msg> should be a present-tense description of what you’ve changed. In this case, “double wall spacing” is fine.
Without the -m flag, git commit will open a nano (or whatever your default text editor is set to) and ask you to type in a commit msg. -m is a bit faster.
Now that you’ve committed, run git push to push your changes to the remote
server. This is how GitHub sees your changes. If you run into any errors at this
step, read the error logs carefully (they often tell you what to do), and Google
if needed.
Finally, go to our GitHub page, click the “Pull Requests” tab, and create a new draft pull request for your branch. When it asks you to fill in the PR description, you can delete the template and write something simple like “Doubled Wall Spacing” Add that screenshot of your waller setup as a comment below your brand new PR. Nice work!
2. ROS CLI Basics
This section is our variation of the ROS 2 Beginner CLI Tools tutorials. We do things slightly differently (and don’t use all of the ROS 2 features described in those tutorials), so this is intended to keep you from having to read all of those docs.
However, those docs are obviously still the source of truth on ROS. Before we get started, read all of the short “Background” sections for these pages:
Understanding ROS 2 nodes (assuming you didn’t look at the Introduction)
Understanding ROS 2 topics (assuming you didn’t look at the Introduction)
Understanding ROS 2 services
Understanding ROS 2 parameters
Understanding ROS 2 actions
The background sections put together are only a couple hundred words, and contain very neat animated diagrams that we can’t recreate here.
Now that you have some background on what ROS is and how it works, let’s explore how we use ROS in our stack. (ROS is used in place of ROS 2 in the rest of these docs, just know that we are referencing ROS 2 every time.)
First, open up our stack (aka. run the simulator), same as you did in the installation guide. (Remember to source ROS2!) Then run (in another terminal, after souring again)
ros2 topic list
to see the list of topics. Let’s look at how robot 2 is moving. Run
ros2 topic echo /planning/trajectory/robot_2
to see what’s being published to that topic. You should see a bunch of different information pertaining to robot 2’s motion.
Now run ros2 topic info on the same topic to see what message type that
topic is publishing, and how many publishers and subscribers are listening to
it. For this topic, the message type is a subset of rj_msgs/, which means we
wrote our own custom .msg file that this topic uses.
Your task for this section is to find the file that defines the message type
used by /planning/trajectory/robot_2. This will take you a long time if
you search for it manually and almost no time if you use a tool like find.
Once you have the right file, figure out the full filepath and add it to your
GitHub PR as a comment. (Hint: read the paragraph above one more time).
3. Building a Position
Background
This section introduces more concepts of ROS and our strategy. You should make sure you have read through and understand “Understanding ROS 2 actions” from part 2 before proceeding!
Let’s first begin by understanding the difference between strategy and planning in our stack. Strategy is responsible for high level decisions, such as robot movement, kicking procedure, robot communication, and referee interaction. Think “here’s what the robot should do given everything that’s happening on the field right now”.
Planning is responsible for taking the instructions from strategy and turning them into trajectories and commands a robot can execute, which are relayed to our physical robots by the radio.
Our Action Server is housed by the Planner node. The Planner node is responsible for turning requests for robot actions into trajectories for the robot to follow.
The Action Clients are created by the AgentActionClient node, which contains some other useful subscriptions to get information about the field and referee.
At any given time, an AgentActionClient is playing a single position.
It creates a RobotFactoryPosition instance and checks for its task,
which it then relays to the planner using ROS actions.
Take a look through agent_action_client.cpp to get a better understanding of this process.
Strategy decisions are delegated to the Positions. This makes sense with respect to soccer—players play differently based on their position.
There are three major positions: Offense, Defense, and Goalie. You may see some others, but these are only for special game cases.
Robots independently make choices on what position to play via the RobotFactoryPosition. The RobotFactoryPosition follows the factory design pattern, as it generates different position instances (e.g., Offense) based on the game state, and returns the relevant intent from whatever position it is playing.
Take some time to read through Offense, Defense, and Goalie, paying special
attention to how they each implement state_to_task and update_state.
This is called a finite state machine, and it is a crucial concept to get the
hang of. Here’s a simple article to get you started: Finite State Machines
Instructions
This is the most open-ended part of the tutorial, but you got this! Remember, if you get stuck, ask Google first. Then, check with your peers. We’re a very collaborative team. If you’re still stuck, your software lead is happy to give you some hints and troubleshoot bugs.
Your task is to create a new position, like Offense, Defense, or Goalie. Your
new position will be called Runner. It will be a subclass of position.hpp.
Some useful C++ resources:
Your runner will be a robot that takes laps around the field. It should run in a rectangle that you choose. If you’re feeling creative, the shape it runs in can be any polygon with 4 or more sides.
A runner’s process looks like this:
Run along first side of shape
Continue until done
Run along second side of shape
Continue until done
Run along third side of shape
Continue until done
The above steps will repeat, and then start over once the robot has finished making the shape you have specified.
Hopefully, you’re seeing how this list lends nicely to a state machine, where states are sides and you know to switch states based on when the robot has reached a vertex (the end of its path).
You will need to look through the other positions to figure out the details of creating this position, but here’s some guidance:
You should locate
offense.cppandoffense.hpp. You will be making a header file for our runner,
so make sure you know the appropriate places to make your files.
* The header file for runner should be quite simple. Start from the most essential components
(overriden methods and private State variables) and work your way up in complexity.
* For readability purposes, let’s keep our states an enum. Don’t forget about a current state variable
and a method to get the next state!
* The motion command for driving in a straight line is "path_target".
* The simulator tells you the coordinates of your cursor—these are the same coordinates you can use in your motion commands.
* Remember, we’re inheriting the position class. There is a useful method that we can use from the position class
to see if we’re done with our current state. Can you see how you can use this to help you get to the next state?
* You will need to add the new file name you create to src/rj_strategy/CMakeLists.txt. See how this is done for other positions.
Testing
To test your new position, the robot(s) needs to know to use it.
Recall that the RobotFactoryPosition (robot_factory_position.cpp) is how the robot assigns itself a position.
Take some time to review this file. RobotFactoryPosition is a subclass of Postion, just like your new runner.
However, it determines what intent to return by calling get_task on the current_position_ instance, which in our case should be your runner!
You only want one Runner robot, so just set the robot with ID 1 to always be a Runner. See how this is done in the constructor with Offense.
You will also need to change other methods as well (i.e. set_default_position) so the position is not overridden on later ticks.
Suggestion: to make it easier to test/focus on robot 1, you can set all other robots to SmartIdle as their default position.
Wrapping up
Make sure that you are periodically commiting your changes. This makes it easy for you to revert things if you need to!
Once robot 1 is successfully running in a rectangle (or other shape), you’re finished! Nice work!
4. SoccerMom
Welcome to the last part of the tutorial! This section is by far the most difficult of the tutorial. However, doing the ROS & C++ introduction will give you a nice amount of background to get this section done, so review your work from that section (or, work on it if you haven’t already).
Objective
In this section, you’ll be creating a SoccerMom node that gets the team color and picks a fruit to match. Our robots have to stay motivated somehow!
You can find the team color by subscribing to the relevant topic (this should
become obvious after looking at the list of topics). To “pick a fruit”, publish
a standard String Msg
to a new topic /team_fruit.
When our team color is yellow, publish “banana” to
/team_fruit.When our team color is blue, publish “blueberries” to
/team_fruit.
Creating a New Node
Note
There are 2 main approaches you can take when adding your files
for the SoccerMom node. The first is to make a separate directory
just for SoccerMom. The second is to directly add your files
into src/rj_radio/src. The first option will teach you a lot about CMake
and how to properly create directories and nodes. However, since this is
an infrequent task, if you do not want to spend too much time exploring CMake,
we recommend option 2.
Often in C++ you’ll see the use of a header file, which ends in .hpp, and a
source file, which ends in .cpp. Header files contain all the function
declarations and docstrings explaining their use. Source files contain the
function definitions–that is, the code that actually makes the functions work.
This allows for many files to share access to the same methods or classes
without copy-pasting their entire implementation by importing the right header
files.
(For more information, check out Headers and Includes resource.)
Let’s take a look at a real example in our codebase to make this more
understandable. Find the radio.cpp and radio.hpp files in our codebase. One of the nodes that
subscribe and publish to various topics is /radio, and these files are the
source of that node.
Comparing the similarities and differences between the subscribers and publishers in these files vs. the ROS introduction will help you learn what you can take directly from the ROS introduction, and where you need to deviate from it.
As a brief overview to help you get started…
Notice the
#includesat the top of both files.#includesare likeimportstatements from Java or Python (with slight differences that are not terribly important for our purposes right now). Using ROS forces you to include certain things; again, check out the ROS tutorial.The header file defines Radio to be subclass of rclcpp::Node (see
public rclcpp::Node). This means the Radio has access to all the methods of rclcpp::Node (notice that Node is undernamespace rclcpp!).The header file also categorizes all variables and methods of the Radio class into
public,protected, andprivate. These are known as “access specifiers”. This article on Access Specifiers sums them up nicely.Both files are enclosed under a namespace. Namespaces are an organizational tool in C++ which helps organize large codebases. For instance, the radio.hpp file defines
namespace radio, so when other files use theSimRadioobject, they referenceradio::SimRadio. Give your SoccerMom node atutorialnamespace.The existing codebase makes heavy use of lambda expressions. For instance, in radio.cpp:
create_subscription<rj_msgs::msg::ManipulatorSetpoint>( control::topics::manipulator_setpoint_topic(i), rclcpp::QoS(1), [this, i](rj_msgs::msg::ManipulatorSetpoint::SharedPtr manipulator) { manipulators_cached_.at(i) = *manipulator; });
Here, a lambda expression is used instead of the callback function that you’ll see in the ROS tutorial. A lambda expression is just a concise way of defining a function without giving it a name. This is only suitable when you know you don’t want to reuse a function (since without a name, you can’t reference that function anywhere else). and requires less lines of code when compared to having another function.
Read more about Lambda Expressions if you would like.
The existing codebase also makes heavy use of pointers. You will see this in the use of the arrow operator,
->. For example:robot_status_topics_.at(robot_id)->publish(robot_status);
The arrow operator is used to access a method or element of an object, when
given a pointer to that object. Above, robot_status_topics_ is a list of
pointers to ROS publisher objects. Calling ->publish(robot_status) on one
element in that list publishes a robot status using that specific publisher.
You will learn more about pointers when you take CS 2110, but if you want to
get a headstart, see C++ Member Operators.
Finally, the docstrings in the radio header file state that the Radio class abstract superclass of the network_radio and sim_radio nodes. (If you are unfamiliar with the concept of abstraction, C++ Abstract Classes is more information.) The concrete subclasses are NetworkRadio and SimRadio.
You might be wondering: okay, this is great, but how do I compile and run my new node?
We use CMake to compile our C++ programs on a variety of different hardware architectures.
You’ll notice that in the rj_radio folder, we have a CMakeLists.txt. This is basically
how we tell the compiler what our files are and how we would like them to be compiled.
As a result, to compile and use your new node, you’ll need to add your new
source files to the right CMake files. It’s okay if you don’t understand everything that’s going on. (Honestly, CMake
files are one of those things we re-learn when adding new nodes and forget
almost immediately after.) Just match the existing patterns if you’re choosing
to build your node within rj_radio.
Launching Your Node
You’re almost there! The final file to get your node up and running is the
.launch file.
Launch files in ROS are a convenient way of starting up multiple nodes, setting
initial parameters, and other requirements. Find the launch/
directory and open the file that seems most relevant to your new node.
Like the CMake section, this part is a lot of copying what already exists and changing it to match your new node’s names. If you want to read more about ROS launch files, the Launch Files Tutorial is a great place to start.
Testing
Whew! What a section. If you’ve made it this far, you should have everything you need to create the SoccerMom node.
This section will probably take you a while. And that’s okay! Remember, you have a large support system to help you out.
Note
Since you have made changes to the C++ part of our codebase, you must build it again to test your node. This may take a while, so be patient and proactive with your changes. If you forgot how to build the codebase, reference the installation section of the tutorial.
To test, change our team color using the UI by going to the top menu bar and
clicking Field > Team Color. You should see the team color change in the top
right corner of our UI. Screenshot proof that your /team_fruit topic is
publishing the right fruit for both options, and post as a comment to your PR.
Similar to the Python section, there’s a lot of file-finding in this part. Use the option in your IDE or text editor that allows you to see a full folder at once. For instance, in VS Code, there is an option to open a full folder, which displays all the subfolders and files in the left toolbar.
If you’ve read this whole section and are feeling a little intimidated, that’s normal. The paragraphs above form a nice guide and checklist for you to follow. Just try your best, one step at a time, and eventually you’ll have a working piece of software to be proud of.
5. Conclusion
Finally, tag your software lead for review on your pull request. For your final comment, leave feedback on anything that confused you in this tutorial. When reviewing your PR, your software lead will either request changes, meaning they have some feedback for you to adjust your PR, or approve it, meaning your changes are ready to merge.
However, this time, upon approval, CLOSE your pull request. Do not merge it. Since this is only a tutorial project, there’s no need to add it to the codebase.
Congratulations! This was a long journey, but if you’ve made it this far, you have proved yourself worthy of your teammates’ trust, and are ready to work on real features. We hope this was a helpful first step in your long robotics career.
7. Resources (again)
Here are all the external links from this document, copied again for your easy reference