📓 Lekcja 02 — Ruch w czasie

(czyli: dlaczego obiekt nie przeskakuje, tylko się porusza) ⏱️

Autor: Asprocool | Kontakt: Asprocool@int.pl

📍 Gdzie jesteśmy?

L00 ✅ → L01 ✅ → L02 ✅ → L03 → L04 → ... → L24 → 🤖 AI

Mamy obiekt z pozycją. Czas dodać prędkość i sprawić żeby się naprawdę poruszał.


🔄 Co nowego w tej lekcji

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

Co się zmienia względem L01:

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)

⚠️ Zanim zaczniesz

Potrzebujesz z L00 i L01:

🎯 Cel lekcji

Po tej lekcji:

Nadal nie mamy:

🏛️ Anegdota historyczna

Galileusz i równomierne przyspieszenie — 1604

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 i dt.
Równanie to samo.

🧠 Problem, który rozwiązujemy

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?"

💡 Dla nowicjusza

Wyobraź sobie że chcesz opisać auto jadące 100 km/h.

Ruch = prędkość × czas.
Bez czasu mamy tylko teleporty.

🧱 Pojęcie nr 1 — prędkość (vx)

💡 Dla nowicjusza

Prędkość to: jak szybko zmienia się pozycja.

Na razie tylko w osi X — jedna liczba:

Prę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

🧱 Pojęcie nr 2 — 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"

💡 Dla nowicjusza

Jeśli auto jedzie 100 km/h i mija 0.5 godziny:

pozycja += 100 km/h × 0.5 h = 50 km

W symulacji:

obj.x += obj.vx * dt  # np. 1.0 * 0.1 = 0.1 jednostki na krok

Czym różni się od 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

🧪 Symulacja ruchu

▶️ Oczekiwany wynik

Pozycja końcowa: x=3.00, y=2.00 Czas symulacji: 3.0s (30 kroków × dt=0.1) Pozycji w historii: 31 ✅ Zapisano 31 kroków do lekcja_02.csv

Linia prosta na wykresie — stała prędkość, brak zakrętów.

Kompletny kod symulacji z wizualizacją i exportem CSV
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("   ...")

🔍 Jak działa ten kod — linia po linii

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.

❓ Częste błędy

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)

🧪 Eksperymenty

💡 Uruchom najpierw główną komórkę z kodem. Potem zmień parametry DT, VX, STEPS albo wklej kod eksperymentu do nowej komórki.


🟢 Eksperyment 1 — wpływ 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.0
  • DT = 0.01 → 30 kroków = 0.3 sekundy symulacji, x końcowy ≈ 0.3

Szczegół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.


🟢 Eksperyment 2 — ujemna prędkość

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).


🟡 Eksperyment 3 — dwa obiekty, różne prędkości

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.0
  • obj_b.x = 2.5 × 0.1 × 20 = 5.0

Dlaczego to ważne: W L21+ będziemy symulować N dział jednocześnie — każde z własną prędkością.


🔴 Eksperyment 4 — save() jako decyzja

Co 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:

Kroków: 50 | Punktów w historii: 10

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.

🧱 Architektura po Lekcji 02

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 fizykix += 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

🔗 Jak to łączy się z następną lekcją?

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.

🧠 Podsumowanie Lekcji 02

Po tej lekcji mamy:

🔜 Co w Lekcji 03?

👉 Lekcja 03 — Ruch w 2D

Dodamy:

🧠 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ąć.

🐍 Nowe rozkazy Pythona w tej lekcji

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 przypisania

obj.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ą.

💡 Dlaczego 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)

📋 Ściągawka — Lekcja 02

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()

✅ Checklista — Ruch w czasie

Sprawdź, czy naprawdę rozumiesz:

🎯 Zadania z charakterem

🟢 Zadanie 1 — Bez czasu

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}")

🟢 Zadanie 2 — Zmiana kroku czasu

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}")

🟡 Zadanie 3 — Stała prędkość

Prędkość vx jest stała przez całą symulację.


🟡 Zadanie 4 — save() jako decyzja

Napisz 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")

🔴 Zadanie 5 — Ujemny krok czasu

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}")

🏆 Zadanie mistrzowskie — Dwa światy

# Ś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")

✅ Odpowiedzi — Checklista

  1. TakObject w L02 ma self.vx = vx w konstruktorze — prędkość pozioma jako część stanu.
  2. Takupdate(obj, dt) wykonuje obj.x += obj.vx * dt, potem obj.save().
  3. Takx += vx * 0 = 0 — pozycja się nie zmienia. dt = 0 zatrzymuje czas.
  4. 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.
  5. Taksave() to narzędzie obserwacji: nie wywołaj jej → ruch się odbywa, historia zostaje pusta.
  6. Taksave() wywołana w __init__history[0] = (x_start, y_start).
  7. Takx += vx * dt = x + (-1.0) * 0.1 = x - 0.1 — x maleje z każdym krokiem.
  8. Takx_końcowe = x_0 + vx × dt × steps. Np. 0 + 2.0 × 0.1 × 30 = 6.0.
  9. Takx += vx × dt. Mniejsze dt → mniejszy przyrost na krok → wolniejszy ruch w tej samej liczbie kroków.
  10. Takvx jest atrybutem obiektu i nie jest modyfikowane przez update(). Zmienia się dopiero gdy w L05+ dodamy przyspieszenie.

🎯 Odpowiedzi — Zadania z charakterem

🟢 Zadanie 1 — Bez czasu

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.

🟢 Zadanie 2 — Zmiana kroku czasu

Wzór update() się nie zmienia — zawsze x += vx * dt. x końcowe różne bo: x = vx × dt × 30 = 1.0 × dt × 30.

Mniejszy dt = gładszy wykres (więcej punktów na jednostkę czasu).

🟡 Zadanie 3 — Stała prędkość

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.

🟡 Zadanie 4 — save() jako decyzja

Historia 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.

🔴 Zadanie 5 — Ujemny krok czasu

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.

🏆 Zadanie mistrzowskie — Dwa światy

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ą.

🧠 Zasada Profesora

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

🔥 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ąć.

📚 Koniec Lekcji 02

Następna lekcja: Lekcja 03 — Ruch w 2D

Autor: Asprocool | Kontakt: Asprocool@int.pl