Intro
that can perform tasks and make decisions by replicating or substituting human actions. Robotics is the scientific field focused on the design and construction of robots. It is a multidisciplinary combination of:
- Electrical Engineering for sensors and actuators. Sensors gather data from the environment, converting physical properties into electrical signals (like our 5 senses). Actuators convert these signals into physical actions or movements (like our muscles).
- Mechanical Engineering for the design and movement of the physical structure.
- Computer Science for the AI software and algorithms.
Currently, ROS (Robot Operating System) is the main framework for Robotics that handles all the different parts of a robot (sensors, motors, controllers, cameras…), where all the components exchange data in a modular way. The ROS framework is meant for real robot prototypes, and it can be used with both Python and C++. Given its popularity, there are many libraries built on top of ROS, like Gazebo, the most advanced 3D simulator.
Since Gazebo is quite complicated, one can still learn the basics of Robotics and build user-friendly simulations outside the ROS ecosystem. The main Python libraries are:
- PyBullet (beginners) — great for experimenting with URDF (Unified Robot Description Format), which is the XML schema for describing robot bodies, parts, and geometry.
- Webots (intermediate) — the physics is more accurate, so it can build more complex simulations.
- MuJoCo (advanced) — real world simulator, it’s used for research-grade experiments. OpenAI’s RoboGYM library is built on top of MuJoCo.

Since this is a tutorial for beginners, I’m going to show how to build simple 3D simulations with PyBullet. I will present some useful Python code that can be easily applied in other similar cases (just copy, paste, run) and walk through every line of code with comments so that you can replicate this example.
Setup
PyBullet is the most user-friendly physics simulator for games, visual effects, Robotics, and Reinforcement Learning. You can easily install it with one of the following commands (if pip fails, conda should definitely work):
pip install pybullet
conda install -c conda-forge pybullet
You can run PyBullet in two main modes:
p.GUI→ opens a window and shows the simulation in real time.p.DIRECT→ no graphics mode, used for scripts.
import pybullet as p
p.connect(p.GUI) #or p.connect(p.DIRECT)

Since this is a physics simulator, the first thing to do is set up gravity:
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
Sets the global gravity vector for the simulation. “-9.8” on the z-axis means an acceleration of 9.8 m/s^2 downward, just like on Earth. Without this, your robot and plane would float in zero-gravity, just like in space.
URDF file
If the robot was a human body, the URDF file would be the skeleton that defines its physical structure. It’s written in XML, and it’s basically the blueprint for the machine, defining what your robot looks like and how it moves.
I’m going to show how to create a simple cube, which is the 3D equivalent of print(“hello world”).
urdf_string = """"
"""
You can either save the XLM code as a URDF file or use it as a string.
import pybullet as p
import tempfile
## setup
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
## create a temporary URDF file in memory
with tempfile.NamedTemporaryFile(suffix=".urdf", mode="w", delete=False) as f:
f.write(urdf_string)
urdf_file = f.name
## load URDF
p.loadURDF(fileName=urdf_file, basePosition=[0,0,1])
## render simulation
for _ in range(240):
p.stepSimulation()

Please note that the loop did run with 240 steps. Why 240? It’s a fixed timestep commonly used in videogame development for a smooth and responsive experience without overtaxing the CPU. 60 FPS (frames per second) with 1 frame displayed every 1/60 of a second, which means that a physics step of 1/240 seconds provides 4 physics updates for every rendered frame.
In the previous code, I rendered the cube with p.stepSimulation(). It means that the simulation is not in real-time and you control when the next physics step happens. Alternatively, you could use time sleep to bind it to the real world clock.
import time
## render simulation
for _ in range(240):
p.stepSimulation() #add a physics step (CPU speed = 0.1 second)
time.sleep(1/240) #slow down to real-time (240 steps × 1/240 second sleep = 1 second)

Going forward, the XML code for robots will get much more complicated. Luckily, PyBullet comes with a collection of preset URDF files. You can easily load the default cube without creating the XML for it.
import pybullet_data
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
p.loadURDF(fileName="cube.urdf", basePosition=[0,0,1])
At its core, a URDF file defines two main things: links and joints. Links are the solid part of the robots (like our bones), and a joint is the connection between two rigid links (like our muscles). Without joints, your robot would be just a statue, but with joints it becomes a machine with motion and purpose.
In a nutshell, every robot is a tree of links connected by joints. Each joint can be fixed (no movement) or rotate (“revolute joint”) and slide (“prismatic joint”). Therefore, you need to know the robot you’re using.
Let’s make an example with the famous robot R2D2 from Star Wars.

Joints
This time, we have to import a plane as well in order to create a ground for the robot. Without a plane, objects would not have a surface to collide with and they would just fall indefinitely.
## setup
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
## load URDF
plane = p.loadURDF("plane.urdf")
robo = p.loadURDF(fileName="r2d2.urdf", basePosition=[0,0,0.1])
## render simulation
for _ in range(240):
p.stepSimulation()
time.sleep(1/240)
p.disconnect()

Before using the robot, we must understand its components. PyBullet has standardized the information structure so that every robot you import shall always be defined by the same 17 universal attributes. Since I just want to run a script, I’m going to connect to the physics server without the graphic interface (p.DIRECT). The main function to analyze a joint is p.getJointInfo().
p.connect(p.DIRECT)
dic_info = {
0:"joint Index", #starts at 0
1:"joint Name",
2:"joint Type", #0=revolute (rotational), 1=prismatic (sliding), 4=fixed
3:"state vectorIndex",
4:"velocity vectorIndex",
5:"flags", #nvm always 0
6:"joint Damping",
7:"joint Friction", #coefficient
8:"joint lowerLimit", #min angle
9:"joint upperLimit", #max angle
10:"joint maxForce", #max force allowed
11:"joint maxVelocity", #max speed
12:"link Name", #child link connected to this joint
13:"joint Axis",
14:"parent FramePos", #position
15:"parent FrameOrn", #orientation
16:"parent Index" #−1 = base
}
for i in range(p.getNumJoints(bodyUniqueId=robo)):
print(f"--- JOINT {i} ---")
joint_info = p.getJointInfo(bodyUniqueId=robo, jointIndex=i)
print({dic_info[k]:joint_info[k] for k in dic_info.keys()})

Every joint, no matter if it’s a wheel, elbow, or gripper, has to expose those same features. The code above shows that R2D2 has 15 joints. Let’s analyze the first one, called “base to right-leg”:
- The joint type is 4, which means it doesn’t move. The parent link is -1, which means it is connected to the base, the root part of the robot (like our skeleton spine). For R2D2, the base is the main cylindrical body, that big blue and white barrel.
- The link name is “right leg”, so we understand that this joint connects the robot’s base with the right leg, but it’s not motorized. That is confirmed by joint axis, joint dumping, and joint friction being all zeros.
- Parent frame position and orientation define where the right leg is attached to the base.
Links
On the other hand, in order to analyze the links (the physical body segments), one must loop through the joints to extract the link name. Then, you can use two main functions: p.getDynamicsInfo() to understand the physical properties of the link, and p.getLinkState() to know its spatial state.
p.connect(p.DIRECT)
for i in range(p.getNumJoints(robo)):
link_name = p.getJointInfo(robo, i)[12].decode('utf-8') #field 12="link Name"
dyn = p.getDynamicsInfo(robo, i)
pos, orn, *_ = p.getLinkState(robo, i)
dic_info = {"Mass":dyn[0], "Friction":dyn[1], "Position":pos, "Orientation":orn}
print(f"--- LINK {i}: {link_name} ---")
print(dic_info)

Let’s analyze the first one, the “right-leg”. The mass of 10 kg contributes to gravity and inertia, while the friction affects how much it slides when contacting the ground. The orientation (0.707=sin(45°)/cos(45°)) and position indicate that the right-leg link is a solid piece, slightly forward (5cm), tilted 90° relative to the base.
Movements
Let’s have a look at a joint that can actually move.
For istance, joint 2 is the right front wheel. It’s a type=0 joint, so it rotates. This joint connects the right leg (link index 1) to the right front wheel: base_link → right_leg → right_front_wheel. The joint axis is (0,0,1), so we know that the wheel spins around the z-axis. The limits (lower=0, upper=-1) indicate that the wheel can spin infinitely, which is normal for rotors.
We can easily move this joint. The main command for controlling actuators on your robot is p.setJointMotorControl2(), it allows to control the force, velocity, and position of a joint. In this case, I want to make the wheel spin, so I will apply force to gradually bring the velocity from zero to a target velocity, then maintain it by balancing applied force and resistive forces (i.e. friction, damping, gravity).
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=2,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=10, force=5)
Now, if we apply the same force to all the 4 wheels, we will make our little robot move forward. From the analysis carried out earlier, we know that the joints for the wheel are number 2 and 3 (right side), and number 6 and 7 (left side).

Considering that, I shall first make R2D2 turn around by spinning only one side, and then apply force to every wheel at the same time to make it move forward.
import pybullet as p
import pybullet_data
import time
## setup
p.connect(p.GUI)
p.resetSimulation()
p.setGravity(gravX=0, gravY=0, gravZ=-9.8)
p.setAdditionalSearchPath(path=pybullet_data.getDataPath())
## load URDF
plane = p.loadURDF("plane.urdf")
robo = p.loadURDF(fileName="r2d2.urdf", basePosition=[0,0,0.1])
## settle down
for _ in range(240):
p.stepSimulation()
right_wheels = [2,3]
left_wheels = [6,7]
### first turn
for _ in range(240):
for j in right_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=-100, force=50)
for j in left_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=100, force=50)
p.stepSimulation()
time.sleep(1/240)
### then move forward
for _ in range(500):
for j in right_wheels + left_wheels:
p.setJointMotorControl2(bodyUniqueId=robo, jointIndex=j,
controlMode=p.VELOCITY_CONTROL,
targetVelocity=-100, force=10)
p.stepSimulation()
time.sleep(1/240)
p.disconnect()

Please note that every robot has a different mass (weight) so the movements can be different based on the simulation physics (i.e. gravity). You can play and try different force and velocity until you find the desired outcome.

Conclusion
This article has been a tutorial to introduce PyBullet and how to create very basic 3D simulations for Robotics. We learned how to import URDF objects and how to analyze joints and links. We also played with the famous robot R2D2. New tutorials with more advanced robots will come.
Full code for this article: GitHub
I hope you enjoyed it! Feel free to contact me for questions and feedback or just to share your interesting projects.
👉 Let’s Connect 👈

(All images are by the author unless otherwise noted)



