(czyli: dlaczego obiekt nie przeskakuje, tylko się porusza) ⏱️
Autor: Asprocool | Kontakt: Asprocool@int.pl
Mamy obiekt z pozycją. Czas dodać prędkość i sprawić żeby się naprawdę poruszał.
Jedna kluczowa nowość:
update(obj, dt)— ruch jako zmiana stanu w czasie.
| Co dodajemy | Typ | Po co |
|---|---|---|
vx |
atrybut Object |
prędkość w osi x |
save() |
metoda Object |
zapisuje aktualną pozycję do historii |
history |
atrybut Object (lista) |
pamięć pozycji — teraz krotki (x, y) |
update(obj, dt) |
funkcja | jeden krok fizyki — przesuwa obiekt o vx * dt |
draw_update(obj) |
funkcja | rysuje trajektorię z historii |
| L01 | L02 |
|---|---|
move_right(obj, step) — ręczna zmiana |
update(obj, dt) — automatyczna, w rytmie czasu |
| brak prędkości | vx — obiekt niesie swoją prędkość |
Object(x, y) |
Object(x, y, vx) |
Potrzebujesz z L00 i L01:
dt — krok czasu symulacjiclass Object z atrybutami x, yself.history jako listę zapisanych stanówmatplotlib, jupyter, numpyPo tej lekcji:
dt w równaniu x += vx * dtupdate() od move_right()Nadal nie mamy:
Zanim Newton sformułował prawa ruchu, Galileusz Galilei prowadził eksperymenty z kulkami toczącymi się po pochylniach. Odkrył coś zaskakującego: droga jest proporcjonalna do kwadratu czasu, nie do czasu.
Ale żeby to odkryć, musiał najpierw rozwiązać prostszy problem: jak mierzyć czas wystarczająco dokładnie? Zegary były wtedy za wolne. Galileusz używał własnego pulsu, a później wynalazł metodę ważenia wody wypływającej z naczynia jako miary czasu.
Kluczowe odkrycie: prędkość i czas to dwie osobne rzeczy. Obiekt może mieć prędkość ale stać w miejscu (w sensie: dt = 0). Obiekt może być w miejscu i mieć czas — ale bez prędkości nie ruszy.
Brzmi znajomo?
obj.x += obj.vx * dt # Galileusz by to rozpoznał
🧠 Galileusz mierzył ruch kulkami i wodą.
My mierzymy go pikselami idt.
Równanie to samo.
W L01 robiliśmy tak:
move_right(obj, 1.0) # skok o 1.0
To NIE jest ruch. To teleport. Brakuje pytania: „ile czasu to zajęło?"
Wyobraź sobie że chcesz opisać auto jadące 100 km/h.
auto.x += 100 — teleport na 100 kmauto.x += 100 * 0.001 — przejechało 0.1 km w ciągu 0.001 godzinyRuch = prędkość × czas.
Bez czasu mamy tylko teleporty.
vx)Prędkość to: jak szybko zmienia się pozycja.
Na razie tylko w osi X — jedna liczba:
vx = 1.0 — porusza się w prawo, 1 jednostka na sekundę symulacjivx = -1.0 — porusza się w lewovx = 0.0 — stoi w miejscuPrędkość to część stanu obiektu — obiekt ją niesie, świat jej nie zmienia (jeszcze).
class Object:
def __init__(self, x, y, vx):
self.x = x
self.y = y
self.vx = vx # ← NOWOŚĆ — prędkość pozioma
update(obj, dt)To jest najważniejsza funkcja kursu. Przez kolejne 20 lekcji będzie rosnąć, ale nigdy nie zniknie.
def update(obj, dt):
obj.x += obj.vx * dt
Czytaj na głos: „pozycja x zwiększa się o prędkość razy czas"
Jeśli auto jedzie 100 km/h i mija 0.5 godziny:
W symulacji:
obj.x += obj.vx * dt # np. 1.0 * 0.1 = 0.1 jednostki na krok
move_right()?move_right(obj, step) |
update(obj, dt) |
|---|---|
| ręczna zmiana — ty decydujesz o skoku | automatyczna — obiekt niesie vx |
| nie ma pojęcia czasu | dt łączy prędkość z czasem |
| wywoływana kiedy chcesz | wywoływana co krok symulacji |
| ruch = skok | ruch = proces w czasie |
Linia prosta na wykresie — stała prędkość, brak zakrętów.
import matplotlib.pyplot as plt
import csv
# ======================
# Klasa Object z prędkością i historią
# ======================
class Object:
def __init__(self, x, y, vx):
self.x = x
self.y = y
self.vx = vx # prędkość w osi x
self.history = []
self.save() # zapisz pozycję startową
def save(self):
"""Zapisz aktualną pozycję do historii."""
self.history.append((self.x, self.y))
# ======================
# Funkcja aktualizacji — jeden krok fizyki
# ======================
def update(obj, dt):
"""Przesuń obiekt o vx * dt. To jest serce symulacji."""
obj.x += obj.vx * dt
obj.save()
# ======================
# Funkcja wizualizacji
# ======================
def draw_update(obj, title="Trajektoria obiektu"):
"""Rysuje historię pozycji jako trajektorię."""
xs = [p[0] for p in obj.history]
ys = [p[1] for p in obj.history]
plt.figure(figsize=(10, 4))
plt.plot(xs, ys, 'b-', linewidth=1.5, alpha=0.6)
plt.scatter(xs, ys, s=30, color='red', zorder=5)
plt.scatter(xs[0], ys[0], s=120, color='green', zorder=6, label='start')
plt.scatter(xs[-1], ys[-1], s=120, color='blue', zorder=6, label='koniec')
plt.xlabel("x")
plt.ylabel("y")
plt.title(title)
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
# ======================
# ⚙️ PARAMETRY EKSPERYMENTU
# ======================
DT = 0.1 # krok czasu
STEPS = 30 # liczba kroków symulacji
VX = 1.0 # prędkość pozioma
# ======================
# Symulacja
# ======================
obj = Object(x=0.0, y=2.0, vx=VX)
for step in range(STEPS):
update(obj, DT)
print(f"Pozycja końcowa: x={obj.x:.2f}, y={obj.y:.2f}")
print(f"Czas symulacji: {STEPS * DT:.1f}s ({STEPS} kroków × dt={DT})")
print(f"Pozycji w historii: {len(obj.history)}")
draw_update(obj, title=f"Ruch: vx={VX}, dt={DT}, {STEPS} kroków")
# ======================
# 💾 Export do CSV
# ======================
# Co robi ten blok:
#
# writer.writerow(['krok', 'x', 'y', 'vx'])
# → nagłówek z czterema kolumnami
#
# enumerate(obj.history)
# → (0, (x, y)), (1, (x, y)), ...
#
# round(x, 6) → unika błędów zmiennoprzecinkowych
with open('lekcja_02.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['krok', 'x', 'y', 'vx'])
for i, (x, y) in enumerate(obj.history):
writer.writerow([i, round(x, 6), round(y, 6), round(obj.vx, 6)])
print(f"\n✅ Zapisano {len(obj.history)} kroków do lekcja_02.csv")
print(" Zawartość:")
print(" krok,x,y,vx")
for i, (x, y) in enumerate(obj.history[:5]):
print(f" {i},{round(x,6)},{round(y,6)},{round(obj.vx,6)}")
print(" ...")
self.vx = vx
Nowy atrybut — prędkość pozioma. Obiekt ją niesie jak własną cechę. Nie zmienia jej sam — zmieni ją dopiero update() albo wpływy w L05+.
self.save() # w __init__
Wywołujemy save() już w konstruktorze — zapisujemy pozycję startową. Dzięki temu history[0] zawsze jest punkt startowy, nie pierwszy krok.
def update(obj, dt):
obj.x += obj.vx * dt
obj.save()
Dwie linijki — cała fizyka tej lekcji. Najpierw przesuwamy (x += vx * dt), potem zapisujemy nową pozycję. Kolejność ma znaczenie — save() po update, nie przed.
DT = 0.1 # ⚙️ PARAMETRY EKSPERYMENTU
Stała na górze kodu — łatwo zmienić, łatwo zobaczyć. Konwencja: parametry eksperymentu = wielkie litery.
for step in range(STEPS):
update(obj, DT)
Pętla symulacji. Każde wywołanie update() = jeden krok czasu. obj zmienia się w miejscu — nie tworzymy nowego obiektu.
| Błąd | Co się dzieje | Jak naprawić |
|---|---|---|
update(obj, 0) lub DT = 0 |
x się nie zmienia (0 × cokolwiek = 0) | Ustaw DT > 0 |
Zapominasz obj.save() w update() |
wykres będzie miał tylko punkt startowy | Dodaj obj.save() na końcu update() |
obj.x = obj.vx * dt zamiast += |
obiekt teleportuje się od 0 | Użyj += |
update(dt, obj) — zła kolejność |
TypeError |
Zawsze update(obj, dt) |
💡 Uruchom najpierw główną komórkę z kodem. Potem zmień parametry DT, VX, STEPS albo wklej kod eksperymentu do nowej komórki.
dt na trajektorięCo robisz: Zmień DT i uruchom ponownie:
DT = 0.5 # duży krok
DT = 0.01 # mały krok
Co zobaczysz:
DT = 0.5 → 30 kroków = 15 sekund symulacji, x końcowy ≈ 15.0DT = 0.01 → 30 kroków = 0.3 sekundy symulacji, x końcowy ≈ 0.3Szczegółowe wyjaśnienie: x = vx × dt × steps = 1.0 × DT × 30. Większe DT → dalej zajdzie w tych samych krokach. Kształt trajektorii (linia prosta) się nie zmienia — zmienia się tylko skala.
Dlaczego to ważne: dt kontroluje tempo świata, nie kształt ruchu. To jeden z kluczowych parametrów symulacji.
Co robisz:
VX = -1.0 # prędkość w lewo
Co zobaczysz: Obiekt przesuwa się w lewo — x maleje z każdym krokiem.
Szczegółowe wyjaśnienie: x += (-1.0) * 0.1 = -0.1 per krok. Po 30 krokach: x = 0 + (-3.0) = -3.0. Znak vx określa kierunek ruchu. Ujemny = w lewo.
Dlaczego to ważne: W L05+ grawitacja będzie ujemnym przyspieszeniem (ay = -g). Oznacza to, że w każdym kroku do prędkości vy dodawana jest ujemna wartość. Gdy lecimy w górę (vy > 0), prędkość spada (pocisk zwalnia). Gdy lecimy w dół (vy < 0), prędkość rośnie co do wartości bezwzględnej (pocisk przyspiesza), ale jej wartość algebraiczna vy nadal maleje (staje się bardziej ujemna).
Co robisz:
obj_a = Object(x=0.0, y=1.0, vx=1.0)
obj_b = Object(x=0.0, y=3.0, vx=2.5)
for _ in range(20):
update(obj_a, DT)
update(obj_b, DT)
plt.figure(figsize=(10, 5))
for obj_plot, color, label in [(obj_a,'red','vx=1.0'), (obj_b,'blue','vx=2.5')]:
xs = [p[0] for p in obj_plot.history]
ys = [p[1] for p in obj_plot.history]
plt.plot(xs, ys, color=color, label=label)
plt.scatter(xs, ys, color=color, s=20)
plt.legend(); plt.grid(True, alpha=0.3); plt.show()
Co zobaczysz: Dwie równoległe linie — niebieska rośnie szybciej (2.5× szybsza).
Szczegółowe wyjaśnienie: obj_b przebywa 2.5× więcej drogi na krok. Po 20 krokach:
obj_a.x = 1.0 × 0.1 × 20 = 2.0obj_b.x = 2.5 × 0.1 × 20 = 5.0Dlaczego to ważne: W L21+ będziemy symulować N dział jednocześnie — każde z własną prędkością.
save() jako decyzjaCo robisz:
# Zapisuj tylko co 5. krok
obj2 = Object(x=0.0, y=0.0, vx=1.0)
obj2.history = [] # wyczyść historię startową
for step in range(50):
update(obj2, DT)
if step % 5 == 0:
obj2.save() # nadpisz save() w pętli
print(f"Kroków: 50 | Punktów w historii: {len(obj2.history)}")
Co zobaczysz:
Szczegółowe wyjaśnienie: save() to narzędzie obserwacji — nie fizyka. Obiekt wykonał 50 kroków, ale zapamiętał tylko 10. Ruch się odbył — historia jest wybiórcza.
Dlaczego to ważne: W dużych symulacjach zapisujemy co N-ty krok żeby oszczędzić pamięć. save() jest kamerą — kamera nie wpływa na ruch.
| Element | Typ | Atrybuty | Metody / Funkcje | Co robi |
|---|---|---|---|---|
World |
klasa | dt, time, history |
step() |
zarządza czasem (z L00) |
Object |
klasa | x, y, vx, history |
save() |
punkt w przestrzeni z prędkością |
update(obj, dt) |
funkcja | — | — | jeden krok fizyki — x += vx * dt |
draw_update(obj) |
funkcja | — | — | rysuje trajektorię |
Jeszcze nie mamy:
| Element | Kiedy |
|---|---|
vy — prędkość pionowa |
L03 |
class Influence, Gravity, Drag |
L06 |
class Cannon, Projectile, fire() |
L11 |
class Replay |
L18 |
class GameRules, GameLoop |
L23–L24 |
Object ma teraz vx ale brak vy — porusza się tylko poziomo. W L03 dodamy vy i ruch stanie się dwuwymiarowy: obiekt będzie poruszać się po płaszczyźnie XY. update() rozszerzy się o y += vy * dt.
Po tej lekcji mamy:
vx — prędkość jako część stanu obiektuupdate(obj, dt) — jeden krok fizyki: x += vx * dtsave() — świadome zapisywanie historii👉 Lekcja 03 — Ruch w 2D
Dodamy:
vy — prędkość pionowaupdate() rozszerzony o y += vy * dt🧠 Złota myśl:
Ruch bez czasu to teleport.
Czas bez ruchu to cisza.
x += vx * dt— właśnie sprawiliśmy że świat zaczął płynąć.
| Rozkaz | Co robi | Przykład |
|---|---|---|
obj.x += obj.vx * dt |
aktualizacja pozycji — serce symulacji | 0.0 += 1.0 * 0.1 → 0.1 |
STALA = wartość |
stała eksperymentu — wielkie litery | DT = 0.1 |
obj.history[0] |
pierwszy element listy — punkt startowy | (0.0, 2.0) |
plt.plot(xs, ys) |
rysuje linię łączącą punkty | trajektoria |
plt.scatter(xs[0], ys[0], color='green') |
wyróżnia start/koniec | zielony punkt |
+= — skrót od dodania i przypisaniaobj.x += obj.vx * dt
# to samo co:
obj.x = obj.x + obj.vx * dt
+= jest krótsze i działa w miejscu — modyfikuje istniejącą zmienną.
save() jest osobną metodą?# ZŁY POMYSŁ — save() automatycznie w update():
def update(obj, dt):
obj.x += obj.vx * dt
obj.history.append((obj.x, obj.y)) # zawsze zapisuje
# DOBRY POMYSŁ — save() to decyzja:
def update(obj, dt):
obj.x += obj.vx * dt
obj.save() # wywołaj kiedy chcesz
Oddzielenie fizyki (update) od obserwacji (save) to zasada przez cały kurs. save() to kamera — kamera nie zmienia fizyki.
history[0] vs history[-1]obj.history[0] # pierwsza pozycja — startowa
obj.history[-1] # ostatnia pozycja — aktualna
obj.history[5] # szósta pozycja (indeksy od 0)
| Co | Jak | Uwaga |
|---|---|---|
| Utwórz obiekt z prędkością | obj = Object(x=0.0, y=0.0, vx=1.0) |
historia zaczyna od [(0,0)] |
| Jeden krok fizyki | update(obj, dt) |
x += vx * dt + save() |
| 50 kroków symulacji | for _ in range(50): update(obj, dt) |
czas: 50 × dt |
| Czas symulacji | steps * dt |
np. 30 × 0.1 = 3.0s |
| Pozycja po N krokach | obj.x |
po pętli |
| Cała trajektoria | obj.history |
lista krotek [(x,y), ...] |
| Kolumny X | [p[0] for p in obj.history] |
list comprehension |
| Kolumny Y | [p[1] for p in obj.history] |
list comprehension |
| Rysuj trajektorię | draw_update(obj) |
historia jako linia |
| Zmień prędkość | obj.vx = 2.0 |
działa od następnego update() |
Sprawdź, czy naprawdę rozumiesz:
Object ma teraz atrybut vx — prędkość w osi x?update(obj, dt) wykonuje obj.x += obj.vx * dt?dt > 0 obiekt się nie porusza?update() od move_right() z L01?save() to narzędzie obserwacji — nie fizyka?history[0] to pozycja startowa?vx oznacza ruch w lewo?x = x0 + vx × dt × steps?dt = wolniejszy ruch w tej samej liczbie kroków?obj.vx pozostaje stałe przez całą symulację (w tej lekcji)?Wyobraź sobie symulację z dt = 0.
Zrób to w Jupyter:
obj = Object(x=0.0, y=0.0, vx=5.0)
for _ in range(100):
update(obj, 0) # dt = 0
print(f"Pozycja po 100 krokach: x={obj.x}")
vx = 5.0 cokolwiek znaczy bez czasu?dt w symulacji?Zrób to w Jupyter:
for dt_test in [0.01, 0.1, 0.5, 1.0]:
obj = Object(x=0.0, y=0.0, vx=1.0)
for _ in range(30):
update(obj, dt_test)
print(f"dt={dt_test}: x końcowe = {obj.x:.2f}")
update() się zmienia?x końcowe jest różne?dt daje gładszą trajektorię?Prędkość vx jest stała przez całą symulację.
x(krok)? Prosta czy krzywa?vx=2.0, dt=0.1, 50 krokach — jaka będzie x końcowa?save() jako decyzjaNapisz symulację gdzie zapisujesz tylko co 10. krok:
obj = Object(x=0.0, y=0.0, vx=1.0)
obj.history = [] # wyczyść historię startową
for step in range(100):
update(obj, 0.1)
if step % 10 == 0:
obj.save()
print(f"Kroków: 100, punktów w historii: {len(obj.history)}")
draw_update(obj, title="Co 10. krok")
Zrób to w Jupyter:
obj = Object(x=5.0, y=0.0, vx=1.0)
for _ in range(50):
update(obj, -0.1) # ujemny dt!
print(f"Pozycja końcowa: x={obj.x:.2f}")
x?# Świat A: mały dt, dużo kroków
obj_a = Object(x=0.0, y=1.0, vx=1.0)
for _ in range(1000):
update(obj_a, 0.01)
# Świat B: duży dt, mało kroków
obj_b = Object(x=0.0, y=3.0, vx=1.0)
for _ in range(10):
update(obj_b, 1.0)
print(f"Świat A: {1000} kroków × dt=0.01 → x={obj_a.x:.2f}, czas={1000*0.01:.1f}s")
print(f"Świat B: {10} kroków × dt=1.0 → x={obj_b.x:.2f}, czas={10*1.0:.1f}s")
dt gdy pojawi się grawitacja (L05)?dt?Object w L02 ma self.vx = vx w konstruktorze — prędkość pozioma jako część stanu.update(obj, dt) wykonuje obj.x += obj.vx * dt, potem obj.save().x += vx * 0 = 0 — pozycja się nie zmienia. dt = 0 zatrzymuje czas.move_right(obj, step) zmienia pozycję o stały skok bez pojęcia czasu. update(obj, dt) łączy prędkość z czasem — ruch jest proporcjonalny do dt.save() to narzędzie obserwacji: nie wywołaj jej → ruch się odbywa, historia zostaje pusta.save() wywołana w __init__ → history[0] = (x_start, y_start).x += vx * dt = x + (-1.0) * 0.1 = x - 0.1 — x maleje z każdym krokiem.x_końcowe = x_0 + vx × dt × steps. Np. 0 + 2.0 × 0.1 × 30 = 6.0.x += vx × dt. Mniejsze dt → mniejszy przyrost na krok → wolniejszy ruch w tej samej liczbie kroków.vx jest atrybutem obiektu i nie jest modyfikowane przez update(). Zmienia się dopiero gdy w L05+ dodamy przyspieszenie.x += 5.0 × 0 = 0 — pozycja się nie zmienia mimo 100 kroków i vx = 5.0. dt = 0 eliminuje powiązanie prędkości z przestrzenią. Prędkość istnieje, ale nie ma czasu w którym mogłaby działać. To potwierdza: ruch = prędkość × czas. Bez czasu są tylko teleporty.
Wzór update() się nie zmienia — zawsze x += vx * dt. x końcowe różne bo: x = vx × dt × 30 = 1.0 × dt × 30.
dt=0.01 → x = 0.30dt=1.0 → x = 30.0Mniejszy dt = gładszy wykres (więcej punktów na jednostkę czasu).
Wykres x(krok) jest linią prostą — każdy krok dodaje tę samą wartość vx × dt. Żeby pojawił się zakręt: vx musiałoby się zmieniać w czasie (przyspieszenie — L05+). Obliczenie: x = 0 + 2.0 × 0.1 × 50 = 10.0.
save() jako decyzjaHistoria ma 10 punktów (kroki 0, 10, 20, ..., 90). Ruch się odbył — 100 kroków wykonane. Historia jest wybiórcza. Wykres wygląda rzadziej (mniej punktów), ale kształt taki sam — linia prosta.
x += 1.0 × (-0.1) = x - 0.1 — x maleje: 5.0 → 4.9 → 4.8 → ... → 0.0. Ruch jest symetryczny względem znaku dt przy stałym vx. Fizycznie: cofanie czasu. Matematycznie poprawne. W praktyce: rzadko używane.
Oba światy mają ten sam czas symulacji: 1000 × 0.01 = 10s = 10 × 1.0. Oba osiągają x = vx × czas = 1.0 × 10 = 10.0 — taką samą pozycję. Świat A ma 1000 obliczeń, Świat B ma 10 — ale wynik identyczny (przy stałym vx). Gdy pojawi się grawitacja (L05): duże dt da błędy numeryczne — krzywizna trajektorii będzie zdeformowana. Idealny dt nie istnieje — zawsze kompromis między dokładnością a szybkością.
Ruch to zmiana stanu względem czasu, nie dodatek do obiektu.
update() to nie dekoracja — to fundament całej fizyki symulacji. Przez kolejne 20 lekcji będzie rosnąć, ale ta jedna linijka zawsze zostanie:
obj.x += obj.vx * dt
Ruch bez czasu to teleport.
Czas bez ruchu to cisza.
x += vx * dt— właśnie sprawiliśmy że świat zaczął płynąć.
📚 Koniec Lekcji 02
Następna lekcja: Lekcja 03 — Ruch w 2D
Autor: Asprocool | Kontakt: Asprocool@int.pl