시뮬레이터는 작은 실험실이다
앞 장까지 우리는 좌표를 숫자로 표현하고, NumPy로 벡터를 계산하고, Matplotlib으로 로봇의 위치를 그려 보았다. 이제 그 조각들을 하나로 묶어 첫 번째 로봇 시뮬레이터를 만들 차례다.
여기서 말하는 시뮬레이터는 거창한 3D 그래픽 프로그램이 아니다. 바퀴의 마찰, 모터 전류, 배터리 전압까지 모두 계산하는 정교한 물리 엔진도 아니다. 이번 장에서 만들 시뮬레이터는 아주 단순하다. 2D 평면 위에 로봇 하나가 있고, 그 로봇은 목표점을 향해 조금씩 움직인다. 우리는 매 순간 로봇의 위치를 계산하고, 그 위치들을 저장한 뒤, 마지막에 궤적으로 그려 본다.
단순해 보이지만 이 구조는 앞으로 계속 반복된다. 로봇공학에서 많은 문제는 결국 다음 질문으로 돌아온다.
"지금 로봇이 어디에 있고, 다음 순간 어디로 가야 하는가?"
이 질문에 답하려면 위치, 방향, 속도, 시간, 목표점이 필요하다. 오늘 우리는 이 다섯 가지를 Python 코드로 직접 다룬다.

로봇의 상태를 변수로 표현하기
현실의 로봇은 복잡하다. 몸체가 있고, 바퀴가 있고, 모터가 있고, 센서가 있다. 하지만 수학과 코드로 로봇을 다룰 때는 먼저 필요한 정보만 뽑아 상태로 표현한다. 상태란 "현재 로봇을 설명하는 숫자들의 묶음"이라고 생각하면 된다.
2D 평면에서 움직이는 가장 단순한 로봇이라면 다음 정보만으로도 꽤 많은 것을 할 수 있다.
위치: 로봇이 평면 위 어디에 있는가
방향: 로봇이 어느 쪽을 바라보고 있는가
속도: 로봇이 얼마나 빠르게 움직이는가
목표점: 로봇이 어디로 가야 하는가
이번 장에서는 위치를 x, y 두 값으로 표현한다. 방향은 각도 theta로 표현할 수 있지만, 처음부터 방향 회전까지 모두 넣으면 코드가 조금 복잡해진다. 그래서 먼저 로봇이 목표점을 향해 바로 움직일 수 있다고 가정하자. 즉, 로봇의 이동 방향은 목표점을 바라보는 방향 벡터로 정한다.
먼저 필요한 라이브러리를 불러오고 초기 상태를 만들어 보자.
import numpy as np
import matplotlib.pyplot as plt
position = np.array([0.0, 0.0]) # 로봇의 현재 위치
target = np.array([8.0, 6.0]) # 목표 위치
speed = 1.0 # 로봇의 속력여기서 position과 target은 모두 2차원 벡터다. position = [0.0, 0.0]은 로봇이 원점에서 출발한다는 뜻이고, target = [8.0, 6.0]은 목표점이 오른쪽으로 8, 위로 6 떨어진 곳에 있다는 뜻이다.
speed는 속력이다. 속도와 속력은 비슷하게 들리지만 약간 다르다. 속력은 "얼마나 빠른가"만 나타내고, 속도는 "어느 방향으로 얼마나 빠른가"까지 포함한다. 예를 들어 1.0은 속력이고, [0.8, 0.6]은 방향이 포함된 속도 벡터다.
이번 시뮬레이터의 핵심은 속력 speed를 목표 방향 벡터와 결합해 속도 벡터를 만드는 것이다.
방향 벡터로 목표를 바라보기
로봇이 목표점까지 가려면 먼저 목표가 어느 방향에 있는지 알아야 한다. 이때 사용하는 가장 기본적인 계산이 "목표 위치에서 현재 위치를 빼기"다.
direction = target - position
print(direction)출력은 다음과 같다.
[8. 6.]이 벡터는 현재 위치에서 목표점으로 향하는 화살표다. 하지만 이 벡터에는 방향뿐 아니라 거리 정보도 함께 들어 있다. [8, 6]의 길이는 10이다. 즉, 목표는 현재 위치에서 10만큼 떨어져 있다.
거리는 벡터의 크기로 구할 수 있다.
distance = np.linalg.norm(direction)
print(distance)np.linalg.norm()은 벡터의 길이를 계산한다. [8, 6]의 길이는 피타고라스 정리에 따라 sqrt(8² + 6²) = 10이 된다.
그런데 로봇이 "방향"만 알고 싶다면 벡터의 길이를 1로 맞추는 것이 좋다. 길이가 1인 벡터를 단위 벡터라고 한다. 방향 벡터를 거리로 나누면 단위 방향 벡터가 된다.
unit_direction = direction / distance
print(unit_direction)출력은 다음과 같다.
[0.8 0.6]이제 [0.8, 0.6]은 목표를 향하는 순수한 방향을 나타낸다. 오른쪽으로 0.8, 위로 0.6의 비율로 움직이면 목표를 향해 똑바로 가게 된다.
이 단위 방향 벡터에 속력 speed를 곱하면 속도 벡터가 된다.
velocity = unit_direction * speed
print(velocity)속력이 1.0이면 속도 벡터도 [0.8, 0.6]이다. 만약 속력을 2.0으로 바꾸면 속도 벡터는 [1.6, 1.2]가 된다. 방향은 같지만 한 번에 더 많이 움직이는 것이다.
시간 간격으로 위치 업데이트하기
시뮬레이션은 시간을 잘게 나누어 진행한다. 현실에서는 시간이 부드럽게 흐르지만, 컴퓨터는 매 순간을 아주 작은 단계로 끊어서 계산한다. 예를 들어 0.1초마다 로봇의 위치를 다시 계산한다고 해 보자.
시간 간격을 보통 dt라고 쓴다. dt는 delta time, 즉 "시간의 작은 변화량"이라는 뜻이다.
위치 업데이트의 기본 공식은 매우 단순하다.
다음 위치 = 현재 위치 + 속도 × 시간 간격Python으로 쓰면 다음과 같다.
dt = 0.1
position = position + velocity * dt만약 속도 벡터가 [0.8, 0.6]이고 dt가 0.1이라면, 한 번의 업데이트에서 위치는 [0.08, 0.06]만큼 이동한다. 아주 조금 움직이는 것이다. 이 작은 이동을 여러 번 반복하면 로봇은 목표점을 향해 부드럽게 이동하는 것처럼 보인다.
여기서 중요한 감각이 있다. 시뮬레이션은 "한 번에 정답 위치로 순간이동하는 것"이 아니다. 작은 시간을 기준으로 조금씩 움직이고, 그 작은 변화들이 쌓여 전체 움직임이 된다. 나중에 미분과 적분을 배울 때도 이 감각이 그대로 이어진다.
한 걸음 움직이는 함수 만들기
코드가 길어지기 전에 한 번의 업데이트를 함수로 묶어 보자. 함수는 현재 위치와 목표 위치를 받아서 다음 위치를 계산한다.
def update_position(position, target, speed, dt):
direction = target - position
distance = np.linalg.norm(direction)
if distance == 0:
return position
unit_direction = direction / distance
velocity = unit_direction * speed
new_position = position + velocity * dt
return new_position이 함수에는 작은 안전장치가 들어 있다.
if distance == 0:
return position현재 위치와 목표 위치가 같으면 거리는 0이다. 이때 direction / distance를 계산하면 0으로 나누게 되므로 문제가 생긴다. 그래서 이미 목표점에 도착했다면 위치를 그대로 반환한다.
하지만 아직 한 가지 문제가 남아 있다. 로봇이 목표점에 거의 도착했는데도 계속 같은 속력으로 움직이면 어떻게 될까? 목표를 지나쳐 버릴 수 있다. 예를 들어 목표까지 남은 거리가 0.05인데 한 번에 0.1만큼 움직이면 목표를 넘어간다. 그다음에는 다시 반대 방향으로 움직이고, 또 넘어가고, 결국 목표 주변에서 흔들릴 수 있다.
이를 막으려면 한 번에 이동할 거리와 남은 거리를 비교해야 한다.
def update_position(position, target, speed, dt):
direction = target - position
distance = np.linalg.norm(direction)
if distance == 0:
return position
step_distance = speed * dt
if step_distance >= distance:
return target.copy()
unit_direction = direction / distance
velocity = unit_direction * speed
new_position = position + velocity * dt
return new_positionstep_distance는 이번 한 번의 업데이트에서 이동할 수 있는 거리다. 만약 이 값이 목표까지 남은 거리보다 크거나 같다면, 로봇을 목표점에 정확히 놓아 버린다. 이렇게 하면 목표점을 지나쳐서 왔다 갔다 하는 현상을 막을 수 있다.
목표 지점까지 이동하는 반복문
이제 로봇이 목표에 도착할 때까지 업데이트를 반복해 보자. 시뮬레이션에서는 보통 while 반복문을 많이 사용한다. 목표에 도착하지 않았으면 계속 움직이고, 목표에 도착하면 멈춘다.
position = np.array([0.0, 0.0])
target = np.array([8.0, 6.0])
speed = 1.0
dt = 0.1
trajectory = [position.copy()]
while True:
position = update_position(position, target, speed, dt)
trajectory.append(position.copy())
distance = np.linalg.norm(target - position)
if distance < 1e-6:
breaktrajectory는 로봇이 지나간 위치들을 저장하는 리스트다. 궤적이라는 뜻이다. 처음 위치를 저장하고, 위치가 업데이트될 때마다 새로운 위치를 복사해서 추가한다.
여기서 position.copy()를 사용하는 이유는 중요하다. NumPy 배열은 객체이기 때문에, 상황에 따라 같은 배열을 계속 참조하게 될 수 있다. 궤적에는 "그 순간의 위치"가 차곡차곡 저장되어야 하므로 복사본을 넣는 습관을 들이는 것이 좋다.
반복문 마지막에서는 목표까지의 거리를 다시 계산한다.
if distance < 1e-6:
break1e-6은 0.000001이다. 컴퓨터는 소수 계산에서 아주 작은 오차가 생길 수 있으므로, distance == 0처럼 정확한 비교만 믿기보다는 "충분히 작으면 도착한 것으로 보자"라고 판단하는 편이 안전하다.
궤적을 배열로 바꾸기
시뮬레이션이 끝나면 trajectory에는 로봇이 지나간 모든 위치가 들어 있다. 하지만 리스트 상태로는 x좌표와 y좌표를 따로 꺼내 그리기 조금 불편하다. NumPy 배열로 바꾸면 훨씬 다루기 쉬워진다.
trajectory = np.array(trajectory)
print(trajectory.shape)출력은 대략 다음과 같은 형태가 된다.
(101, 2)첫 번째 숫자 101은 저장된 위치의 개수다. 두 번째 숫자 2는 각 위치가 x, y 두 값으로 이루어져 있다는 뜻이다. 즉, trajectory는 다음과 같은 구조다.
[
[0.00, 0.00],
[0.08, 0.06],
[0.16, 0.12],
...,
[8.00, 6.00]
]이제 모든 x좌표는 trajectory[:, 0], 모든 y좌표는 trajectory[:, 1]로 꺼낼 수 있다.
xs = trajectory[:, 0]
ys = trajectory[:, 1]이 문법은 처음 보면 낯설 수 있다. :는 "모든 행"을 뜻하고, 0은 첫 번째 열을 뜻한다. 그래서 trajectory[:, 0]은 모든 위치에서 x값만 모은 배열이다.
궤적으로 결과 검증하기
이제 로봇이 정말 목표를 향해 갔는지 그림으로 확인해 보자.
plt.figure(figsize=(6, 6))
plt.plot(trajectory[:, 0], trajectory[:, 1], "b.-", label="trajectory")
plt.scatter(position[0], position[1], c="blue", s=80, label="final position")
plt.scatter(target[0], target[1], c="red", s=100, marker="*", label="target")
plt.axis("equal")
plt.grid(True)
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()파란 선은 로봇이 지나간 경로이고, 빨간 별은 목표점이다. 로봇이 원점에서 출발해 목표점까지 직선으로 이동했다면 시뮬레이터가 제대로 작동한 것이다.
여기서 그림은 단순한 장식이 아니다. 로봇공학에서 시각화는 디버깅 도구다. 숫자만 보면 position = [8.0, 6.0]이라고 출력되어도 움직임이 자연스러웠는지 알기 어렵다. 하지만 궤적을 그리면 로봇이 목표를 지나쳤는지, 이상한 방향으로 갔는지, 중간에 멈췄는지 바로 보인다.
좋은 로봇 코드는 계산만 잘하는 코드가 아니다. 자기 계산을 확인할 수 있는 코드다.
전체 코드 한 번에 보기
지금까지의 내용을 하나로 합치면 다음과 같다.
import numpy as np
import matplotlib.pyplot as plt
def update_position(position, target, speed, dt):
direction = target - position
distance = np.linalg.norm(direction)
if distance == 0:
return position
step_distance = speed * dt
if step_distance >= distance:
return target.copy()
unit_direction = direction / distance
velocity = unit_direction * speed
new_position = position + velocity * dt
return new_position
position = np.array([0.0, 0.0])
target = np.array([8.0, 6.0])
speed = 1.0
dt = 0.1
trajectory = [position.copy()]
while True:
position = update_position(position, target, speed, dt)
trajectory.append(position.copy())
distance = np.linalg.norm(target - position)
if distance < 1e-6:
break
trajectory = np.array(trajectory)
plt.figure(figsize=(6, 6))
plt.plot(trajectory[:, 0], trajectory[:, 1], "b.-", label="trajectory")
plt.scatter(trajectory[0, 0], trajectory[0, 1], c="green", s=80, label="start")
plt.scatter(position[0], position[1], c="blue", s=80, label="final position")
plt.scatter(target[0], target[1], c="red", s=120, marker="*", label="target")
plt.axis("equal")
plt.grid(True)
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()이 코드는 작지만 완전한 시뮬레이터다. 로봇의 상태가 있고, 목표점이 있고, 시간 간격에 따른 업데이트가 있고, 이동 결과를 시각화한다. 앞으로 이 코드에 방향, 회전, 센서 노이즈, 위치 추정, 경로 최적화 같은 개념을 하나씩 붙여 나가게 된다.
로봇을 조금 더 로봇답게 그리기
지금 그림에서는 로봇이 점으로만 표시된다. 조금 아쉽다. 로봇이 어디를 향해 움직이는지 화살표로 표시하면 훨씬 직관적이다.
마지막 위치에서 목표 방향을 계산해 화살표를 그려 보자. 목표에 도착한 뒤에는 방향 벡터가 거의 0이므로, 여기서는 궤적의 마지막 두 점을 이용해 이동 방향을 구한다.
last_position = trajectory[-1]
prev_position = trajectory[-2]
heading = last_position - prev_position
heading = heading / np.linalg.norm(heading)
plt.figure(figsize=(6, 6))
plt.plot(trajectory[:, 0], trajectory[:, 1], "b.-", label="trajectory")
plt.scatter(target[0], target[1], c="red", s=120, marker="*", label="target")
plt.arrow(
last_position[0], last_position[1],
heading[0] * 0.8, heading[1] * 0.8,
head_width=0.25,
length_includes_head=True,
color="blue"
)
plt.axis("equal")
plt.grid(True)
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()plt.arrow()는 시작점과 방향 성분을 받아 화살표를 그린다. 여기서는 마지막 위치에서 시작해 이동 방향으로 짧은 화살표를 그렸다. 이 화살표를 보면 로봇이 어떤 방향으로 움직여 왔는지 한눈에 알 수 있다.
물론 실제 로봇은 목표점에 도착하면 그 방향을 계속 바라볼 수도 있고, 멈춘 뒤 다른 방향으로 회전할 수도 있다. 지금은 단순화를 위해 "이동 방향이 곧 로봇의 방향"이라고 생각하자. 이후 장에서 각도와 atan2, 회전 행렬을 배우면 로봇의 방향을 더 정교하게 다룰 수 있다.
속도를 바꾸면 무엇이 달라질까
시뮬레이터의 장점은 같은 실험을 여러 번 쉽게 반복할 수 있다는 것이다. speed 값을 바꿔 보자.
speed = 0.5속력을 낮추면 로봇은 더 많은 업데이트를 거쳐 목표점에 도착한다. 궤적의 선 모양은 여전히 직선이지만, 저장되는 점의 개수가 늘어난다.
반대로 속력을 높여 보자.
speed = 2.0속력이 높아지면 한 번의 업데이트에서 더 멀리 이동하므로 점의 개수가 줄어든다. 목표까지 가는 실제 경로는 같지만, 시뮬레이션이 기록하는 중간 위치는 더 성긴 간격으로 남는다.
이번에는 dt를 바꿔 보자.
dt = 0.5dt가 커지면 시간 간격이 커진다. 같은 속력이라도 한 번에 이동하는 거리가 커진다. 반대로 dt가 작아지면 더 촘촘하게 움직임을 계산한다.
이 둘은 항상 함께 생각해야 한다.
한 번에 이동하는 거리 = 속력 × 시간 간격속력이 커져도 한 번에 많이 움직이고, 시간 간격이 커져도 한 번에 많이 움직인다. 그래서 시뮬레이션에서는 speed뿐 아니라 dt도 매우 중요하다. 너무 큰 dt는 움직임을 거칠게 만들고, 너무 작은 dt는 계산을 많이 하게 만든다.
목표점을 바꿔 실험하기
이제 목표점을 여러 위치로 바꿔 보자.
target = np.array([-4.0, 7.0])또는 아래쪽으로 보낼 수도 있다.
target = np.array([5.0, -3.0])코드를 다시 실행하면 로봇은 새로운 목표점을 향해 움직인다. 이때 우리가 바꾼 것은 목표점 하나뿐이다. 나머지 계산은 그대로 작동한다. 이것이 벡터 계산의 힘이다. 목표가 오른쪽 위에 있든, 왼쪽 위에 있든, 아래쪽에 있든 target - position은 언제나 현재 위치에서 목표로 향하는 방향을 알려 준다.
로봇공학에서 좋은 수학 표현은 상황이 바뀌어도 같은 방식으로 작동한다. "오른쪽이면 x를 더하고, 왼쪽이면 x를 빼고, 위쪽이면 y를 더하고……"처럼 경우를 일일이 나누지 않아도 된다. 벡터 하나가 그 일을 대신해 준다.
시뮬레이션을 함수로 정리하기
여러 목표점과 속력을 실험하려면 전체 시뮬레이션을 함수로 묶는 편이 좋다. 이제 simulate_robot() 함수를 만들어 보자.
def simulate_robot(start, target, speed=1.0, dt=0.1):
position = start.copy()
trajectory = [position.copy()]
while True:
position = update_position(position, target, speed, dt)
trajectory.append(position.copy())
distance = np.linalg.norm(target - position)
if distance < 1e-6:
break
return np.array(trajectory)이 함수는 시작점, 목표점, 속력, 시간 간격을 받아 궤적을 반환한다. 이제 실험이 훨씬 간단해진다.
start = np.array([0.0, 0.0])
target = np.array([8.0, 6.0])
trajectory = simulate_robot(start, target, speed=1.0, dt=0.1)시각화도 함수로 묶을 수 있다.
def plot_trajectory(trajectory, target):
plt.figure(figsize=(6, 6))
plt.plot(trajectory[:, 0], trajectory[:, 1], "b.-", label="trajectory")
plt.scatter(trajectory[0, 0], trajectory[0, 1], c="green", s=80, label="start")
plt.scatter(target[0], target[1], c="red", s=120, marker="*", label="target")
plt.scatter(trajectory[-1, 0], trajectory[-1, 1], c="blue", s=80, label="final")
plt.axis("equal")
plt.grid(True)
plt.xlabel("x")
plt.ylabel("y")
plt.legend()
plt.show()이제 전체 실행 코드는 이렇게 짧아진다.
start = np.array([0.0, 0.0])
target = np.array([8.0, 6.0])
trajectory = simulate_robot(start, target, speed=1.0, dt=0.1)
plot_trajectory(trajectory, target)함수로 정리하면 코드를 재사용하기 쉬워진다. 앞으로 우리는 이 구조를 계속 확장할 것이다. 목표점이 하나가 아니라 여러 개일 수도 있고, 중간에 장애물이 있을 수도 있고, 센서값에 노이즈가 섞일 수도 있다. 그래도 기본 뼈대는 같다. 상태를 업데이트하고, 기록하고, 확인한다.
이번 장에서 만든 시뮬레이터의 의미
이번 장의 로봇은 아주 단순하다. 목표를 향해 직선으로 움직이고, 장애물도 없고, 바퀴의 회전 제한도 없고, 센서 오차도 없다. 현실의 로봇과 비교하면 장난감처럼 보일 수 있다.
하지만 이 작은 시뮬레이터 안에는 로봇공학의 중요한 구조가 이미 들어 있다.
로봇의 현재 상태를 숫자로 표현했다.
목표와 현재 위치의 차이로 방향을 계산했다.
속도와 시간 간격으로 다음 위치를 업데이트했다.
지나간 위치를 저장해 궤적으로 시각화했다.
이 네 가지는 앞으로 계속 등장한다. 좌표계를 배울 때도, 회전 행렬을 배울 때도, 미분과 적분을 배울 때도, 칼만 필터를 배울 때도, 최적화를 배울 때도 결국 로봇의 상태를 계산하고 다음 상태를 예측한다.
수학은 종이 위의 공식으로만 남아 있으면 멀게 느껴진다. 하지만 position = position + velocity * dt처럼 코드가 되어 움직이는 점을 만들면 갑자기 손에 잡히기 시작한다. 우리는 이제 로봇 수학을 실험할 작은 세계를 얻었다.
다음 실험을 위한 작은 과제
이번 장의 코드를 직접 실행했다면, 다음 실험을 해 보자. 목표는 코드를 외우는 것이 아니라 숫자가 움직임을 어떻게 바꾸는지 느끼는 것이다.
speed를0.2,1.0,3.0으로 바꿔 궤적의 점 간격을 비교해 보자.dt를0.01,0.1,0.5로 바꿔 움직임이 얼마나 촘촘하게 기록되는지 확인해 보자.시작점을
[2.0, -3.0], 목표점을[-5.0, 4.0]처럼 바꿔도 코드가 그대로 작동하는지 확인해 보자.
그리고 한 가지를 꼭 기억하자. 로봇 시뮬레이션은 처음부터 완벽할 필요가 없다. 작은 점 하나가 목표를 향해 움직이는 장면에서 시작해도 충분하다. 그 점에 방향을 붙이면 로봇이 되고, 센서를 붙이면 관측이 되고, 오차를 넣으면 확률 문제가 되고, 더 좋은 경로를 찾기 시작하면 최적화 문제가 된다.
이제 우리는 첫 번째 로봇을 움직였다. 다음부터는 이 로봇이 살아가는 세계를 더 정확하게 표현해 볼 것이다. 위치를 숫자로 표현하는 법부터 다시 단단히 다지면서, 좌표와 벡터의 감각을 로봇의 눈높이에서 익혀 보자.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.