Lightweight 2D Robot Simulator in Python for Beginners
A compact 2D robot simulator is a great way to learn robotics concepts (kinematics, sensors, control) without heavy tools. This tutorial walks you through building a minimal, interactive simulator in Python using Pygame. You’ll get a moving robot, simple sensors, and a basic control loop — all in under 200 lines of code.
What you’ll build
- A window showing a circular robot that can move and rotate.
- Keyboard control plus a simple autonomous controller.
- A basic range sensor (simulated lidar) that detects distances to walls.
- On-screen visualization of sensor rays and obstacles.
Requirements
- Python 3.8+ installed
- Pygame: install with
Code
pip install pygame
Design overview
- World: rectangular arena with rectangular obstacles.
- Robot state: (x, y, theta, v, omega).
- Motion model: differential-drive-style kinematics (velocity + angular velocity update).
- Sensor: cast rays from robot at fixed angular offsets, detect intersection with walls/obstacles.
- Loop: handle input, update physics, sense, draw, repeat at fixed timestep.
Complete code
python
# lightweight_2d_robot_sim.py import math, sys import pygame # Config WIDTH, HEIGHT = 800, 600 ROBOT_RADIUS = 15 MAX_SPEED = 120.0# pixels/sec MAX_OMEGA = 3.0 # rad/sec DT = 1/60.0 # simulation timestep SENSOR_RANGE = 200 SENSOR_ANGLE_SPREAD = math.radians(120) NUM_RAYS = 15 # Obstacles: list of pygame.Rect OBSTACLES = [ pygame.Rect(200, 150, 120, 30), pygame.Rect(450, 350, 200, 40), pygame.Rect(100, 400, 80, 120), ] # Helpers def clamp(v, lo, hi): return max(lo, min(hi, v)) def rotate_point(px, py, ox, oy, theta): s, c = math.sin(theta), math.cos(theta) dx, dy = px-ox, py-oy return ox + cdx - sdy, oy + sdx + cdy def ray_intersect_segment(p, d, a, b): # p: origin, d: dir (normalized), segment a->b. returns distance or None. vx, vy = b[0]-a[0], b[1]-a[1] wx, wy = a[0]-p[0], a[1]-p[1] denom = d[0]vy - d[1]vx if abs(denom) < 1e-6: return None t = (vxwy - vywx) / denom u = (d[0]wy - d[1]wx) / denom if t >= 0 and 0 <= u <= 1: return t return None def cast_ray(origin, angle, world_lines, max_range): dx, dy = math.cos(angle), math.sin(angle) best = max_range for (a,b) in world_lines: d = ray_intersect_segment(origin, (dx,dy), a, b) if d is not None and d < best: best = d hit = (origin[0]+dxbest, origin[1]+dybest) return best, hit def rect_to_lines(rect): x,y,w,h = rect return [((x,y),(x+w,y)), ((x+w,y),(x+w,y+h)), ((x+w,y+h),(x,y+h)), ((x,y+h),(x,y))] # Build world lines (walls + obstacles) def build_world(): lines = [] margin = 0 walls = [pygame.Rect(margin, margin, WIDTH-2margin, HEIGHT-2margin)] for w in walls + OBSTACLES: lines += rect_to_lines(w) return lines # Robot class class Robot: def init(self, x, y, theta=0.0): self.x = x; self.y = y; self.theta = theta self.v = 0.0; self.omega = 0.0 def step(self, dt): # simple integrator self.theta += self.omega dt self.x += self.v math.cos(self.theta) dt self.y += self.v math.sin(self.theta) dt # keep inside bounds self.x = clamp(self.x, ROBOT_RADIUS, WIDTH-ROBOT_RADIUS) self.y = clamp(self.y, ROBOT_RADIUS, HEIGHT-ROBOT_RADIUS) def sensor_readings(self, world_lines): readings = [] start = -SENSOR_ANGLE_SPREAD/2 for i in range(NUM_RAYS): a = self.theta + start + i(SENSOR_ANGLE_SPREAD/(NUM_RAYS-1)) dist, hit = cast_ray((self.x,self.y), a, world_lines, SENSOR_RANGE) readings.append((a, dist, hit)) return readings # Simple autonomous controller: go forward, turn away from nearest obstacle def avoid_controller(robot, readings): # find closest ray min_d = SENSOR_RANGE; mina = None for a,d, in readings: if d < min_d: min_d = d; min_a = a # set forward speed proportional to distance robot.v = MAX_SPEED (min(1.0, (min_d / 100.0))) # steer away: if obstacle on left (angle>theta) turn right, etc. if min_a is not None: ang_err = ((min_a - robot.theta + math.pi) % (2math.pi)) - math.pi # desired turn away: if obstacle ahead, steer opposite sign robot.omega = clamp(-0.8ang_err, -MAX_OMEGA, MAX_OMEGA) else: robot.omega = 0.0 def main(): pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) clock = pygame.time.Clock() world_lines = build_world() robot = Robot(WIDTH0.2, HEIGHT0.5, 0.0) autonomous = False font = pygame.font.SysFont(None, 20) while True: for ev in pygame.event.get(): if ev.type == pygame.QUIT: pygame.quit(); sys.exit() elif ev.type == pygame.KEYDOWN: if ev.key == pygame.K_SPACE: autonomous = not autonomous if ev.key == pygame.K_r: robot = Robot(WIDTH0.2, HEIGHT0.5, 0.0) keys = pygame.key.get_pressed() if not autonomous: # teleop if keys[pygame.K_UP]: robot.v = clamp(robot.v + 200DT, -MAX_SPEED, MAX_SPEED) elif keys[pygame.K_DOWN]: robot.v = clamp(robot.v - 200DT, -MAX_SPEED, MAX_SPEED) else: # simple friction robot.v = 0.95 if keys[pygame.K_LEFT]: robot.omega = -MAX_OMEGA elif keys[pygame.K_RIGHT]: robot.omega = MAX_OMEGA else: robot.omega = 0.0 robot.step(DT) readings = robot.sensor_readings(world_lines) if autonomous: avoid_controller(robot, readings) # DRAW screen.fill((30,30,30)) # obstacles for r in OBSTACLES: pygame.draw.rect(screen, (200,60,60), r) # robot pygame.draw.circle(screen, (100,200,255), (int(robot.x), int(robot.y)), ROBOT_RADIUS) # heading line hx = robot.x + math.cos(robot.theta)ROBOT_RADIUS hy = robot.y + math.sin(robot.theta)ROBOT_RADIUS pygame.draw.line(screen, (0,0,0), (robot.x,robot.y), (hx,hy), 2) # sensors for a,d,hit in readings: color = (100,255,100) if d < 50 else (180,180,180) pygame.draw.line(screen, color, (robot.x,robot.y), hit, 1) pygame.draw.circle(screen, color, (int(hit[0]), int(hit[1])), 2) # HUD mode = “AUTO” if autonomous else “TELEOP” text = font.render(f”Mode: {mode} Pos: ({int(robot.x)},{int(robot.y)}) V:{int(robot.v)}“, True, (240,240,240)) screen.blit(text, (8,8)) pygame.display.flip() clock.tick(60) if name == ”main“: main()
How to use
- Run: python lightweight_2d_robot_sim.py
- Controls:
- Arrow keys: drive manually (up/down to change speed, left/right to rotate).
- Space: toggle autonomous obstacle-avoidance mode.
- R: reset robot to start.
Extensions and learning next steps
- Add collision detection and response with obstacles.
- Replace ray sensor with Gaussian-noise lidar or simulated depth camera.
- Implement PID or path-following controllers.
- Export robot pose logs for offline analysis or training.
This lightweight simulator gives a hands-on environment to test control ideas, sensor processing, and simple navigation without heavy dependencies.
Leave a Reply