Přeskočit na obsah

Matematika, náhoda a kolize

Trojice modulů, o kterou se opírá každá hra na PicoPadu při pohybu, spawnování a detekci zásahů. Jsou drobné, šetří alokacemi a stačí je naimportovat (žádné nastavování enginu). Holé signatury najdeš v /cs/reference/; tahle stránka je o tom proč a kdy.

Co to je: malí číselní pomocníci, po kterých saháš v každém frame - clamp/lerp/approach/wrap, trigonometrie po otáčkách (úhly jako 0..1 otáčky místo radiánů) a 2D vektorová matematika (zabalená sem ze starého picogame_vec). Naimportuj ho jednou jako m a používej na vyhlazování, míření, screen-wrap a vzdálenosti.

Kdy ho použít: kdykoli bys jinak ručně psal clamp, plynulé sledování nebo atan2. Po trigonometrii s _t sáhni, když chceš úhly, které čistě wrapují v [0,1) (stav rotace, míření), místo radiánů.

API:

  • clamp(v, lo, hi) - vrátí v připnuté do [lo, hi]. Každodenní pomocník na udržení pozice na obrazovce nebo HP v rozsahu.
  • mid(a, b, c) - medián tří hodnot; chová se jako clamp, když je prostřední argument hodnota.
  • lerp(a, b, t) - lineární prolnutí a + (b-a)*t. S malým t v každém frame dostaneš plynulé sledování/ease.
  • inv_lerp(a, b, v) - inverze lerp: kde leží v v [a,b] jako 0..1. Vrátí 0.0, pokud a == b.
  • remap(v, a, b, c, d) - namapuje v z rozsahu [a,b] na [c,d]. Bezpečné, když a == b (namapuje na c).
  • sgn(x) - znaménko jako -1, 0 nebo 1.
  • approach(v, target, step) - posune v směrem k target nejvýš o step, nikdy nepřestřelí. Skvělé na tření/akceleraci k nějaké hodnotě.
  • wrap(v, lo, hi) - zabalí v do polootevřeného rozsahu [lo, hi). Degenerované nebo obrácené rozsahy vrátí lo (žádné dělení nulou, nikdy mimo rozsah).
  • sin_t(turns) / cos_t(turns) - sinus/kosinus úhlu v OTÁČKÁCH (1.0 = celý kruh). Kladná hodnota je po směru hodinových ručiček na obrazovce s osou y dolů.
  • atan2_t(dy, dx) - úhel vektoru (dx, dy) v otáčkách, normalizovaný do 0..1. Použij na míření.
  • length(dx, dy) - velikost vektoru.
  • distance(x1, y1, x2, y2) - vzdálenost mezi dvěma body.
  • normalize(dx, dy) - jednotkový vektor; pro vstup nulové délky vrátí (0.0, 0.0).
  • angle_rad(dx, dy) - úhel v RADIÁNECH (surový atan2); from_angle_rad(a, mag=1.0) - vektor délky mag pod radiánovým úhlem a.
  • TAU - konstanta 2*pi, kterou interně používá trigonometrie s _t.

Příklad (rotace lodi a tah po její špičce, jako v asteroids):

import picogame_math as m
ang = 0.0 # heading, in turns 0..1
TURN = 0.01
ang = (ang + TURN) % 1.0 # rotate right
dx, dy = m.sin_t(ang), -m.cos_t(ang) # nose-up direction
vx += dx * 0.25
vy += dy * 0.25
sp = m.length(vx, vy) # current speed
x = m.clamp(x, 8, W - 8) # keep on screen

Záludnosti:

  • Trigonometrie s _t je po směru hodinových ručiček na obrazovce s osou y dolů a “nahoru” je -cos_t - obrať znaménko y složky, jako u lodi v asteroids výše, jinak se ti sprite otáčí špatným směrem.
  • wrap je polootevřený: wrap(hi, lo, hi) vrátí lo, ne hi. Pro obrácený nebo nulový rozsah prostě vrátí lo, místo aby vyhodil chybu.
  • Otáčky (sin_t/cos_t/atan2_t) a radiány (angle_rad/from_angle_rad) používej konzistentně; pro stejný úhel je nemíchej dohromady.

Co to je: drobný, seedovatelný xorshift32 RNG plus pomocníci, které malé hry doopravdy potřebují (vážené výběry, in-place zamíchání, zamíchávací “bag”). Použij ho místo random ze stdlib, když chceš reprodukovatelné běhy - replaye, ghosty, seedy denních výzev nebo deterministické rozložení levelů.

Kdy ho použít: vytvoř jeden Rand na hru (seedni ho kvůli reprodukovatelnosti, nebo seed vynech pro běh seedovaný časem) a veškerou náhodu ber z něj. Po Bag sáhni, když chceš férovost bez šňůr (spawny, díly ve stylu Tetrisu).

Rand(seed=None):

  • Rand(1234) seedne z intu (reprodukovatelné); Rand() seedne z hodin. seed=0 se interně přemapuje (xorshift nemůže začít na nule).
  • seed(s) - znovu seedne existující generátor.
  • below(n) - celé číslo v 0 .. n-1. Vrátí 0, pokud n <= 0.
  • randint(a, b) - celé číslo v a .. b včetně. Vyhodí ValueError, pokud b < a.
  • random() - float v [0.0, 1.0).
  • chance(p) - True s pravděpodobností p (kde p je 0..1).
  • choice(seq) - jeden prvek ze seq. Vyhodí ValueError u prázdné sekvence.
  • shuffle(lst) - Fisher-Yates zamíchání lst, in place (vrací None).
  • weighted(weights) - vrátí index 0..len-1 vybraný úměrně k weights. Vyhodí ValueError, pokud je součet <= 0. Bez řízení šňůr (nezávislé tahy).

Bag(items, rng):

  • Zamíchávací bag / “7-bag”: vydá každou položku jednou za cyklus v zamíchaném pořadí, takže žádné dlouhé šňůry ani sucha. Férovější než nezávislé výběry pro spawny a díly.
  • next() - vrátí další položku, na začátku každého cyklu automaticky zamíchá. Vyhodí ValueError při konstrukci, pokud je items prázdné.

Příklad (jeden seedovaný RNG pro celou hru plus spawnovací bag proti šňůrám):

import picogame_rand
rng = picogame_rand.Rand(0x1234) # seeded -> reproducible
x = rng.randint(40, W - 40) # 40 .. W-40 inclusive
if rng.chance(0.25):
spawn_powerup(x)
kind = rng.weighted([5, 3, 1]) # index 0 most likely
bag = picogame_rand.Bag([0, 1, 2, 3, 4, 5, 6], rng)
piece = bag.next() # every value once per cycle

Záludnosti:

  • shuffle mutuje seznam in place a vrací None - nepiš lst = rng.shuffle(lst).
  • Bag si bere do vlastnictví kopii items; next() přemíchává tento vnitřní seznam, takže pořadí, které jsi předal, se nezachová.
  • weighted vrací index, ne hodnotu - indexuj jím do svého vlastního seznamu.
  • Stejný seed plus stejná sekvence volání rovná se stejné výsledky: o to právě jde, ale taky to znamená, že omylem sdílený rng propojí náhodu dvou systémů.

Co to je: kolizní testy bez alokace, které čtou pozice a velikosti přímo ze spritů, takže přestaneš v každé entitní smyčce ručně psát abs(a.x - b.x) < w and .... Sahají jen na celočíselné gettery (sprite.x, sprite.y, bitmap.width, bitmap.height) a nikdy ne na sprite.anchor (jehož getter alokuje), takže je bezpečné je volat mnohokrát za frame. Per-frame rozpočet, který tohle chrání, najdeš v /cs/hardware/.

Kdy ho použít: uvnitř svých kolizních smyček. Použij hit na překryv boxů, hit_point na test holé souřadnice proti spritu a is_within na levné kruhové testy dosahu (výbuchy, magnetické sběry, blízkost).

API:

  • hit(a, b, hw=None, hh=None) - překryv os zarovnaných boxů (AABB) dvou spritů. hw/hh jsou poloviční rozměry kombinovaného boxu; výchozí je polovina součtu velikostí obou bitmap (test překryvu středů). Vrací bool.
  • hit_point(a, px, py, hw=None, hh=None) - je bod (px, py) v rámci polovičních rozměrů spritu od jeho pozice? Výchozí poloviční rozměry jsou polovina velikosti bitmapy spritu.
  • is_within(a, b, r) - kruhový test: jsou pozice obou spritů v poloměru r? Používá kvadrát vzdálenosti, takže žádný sqrt.

Pozice se berou jako referenční body spritů: dokud oba sprity sdílejí stejný anchor (oba střed, nebo oba levý horní roh), offset anchoru se vyruší a tyhle fungují bez ohledu na to, který anchor jsi zvolil - viz /cs/scene-format/ ohledně anchorů.

Příklad (střely vs kameny kruhově, hráč vs nepřítel boxově - upraveno z asteroids a plošinovky):

import picogame_collide as collide
for b in bullets:
for r in rocks:
if b.visible and collide.is_within(b, r, 18): # circular, no sqrt
kill(b, r)
if collide.hit(player, enemy, 12, 16): # explicit half-extents
take_damage()

Záludnosti:

  • Tyhle čtou živé pozice spritů. Pokud sleduješ pohyb ve floatech (data["x"]), zavolej před testem sprite.move(int(x), int(y)), jinak kolize zaostává o frame za viditelným spritem.
  • hit je test boxů, ne pixel-perfect - dolaď hw/hh (často menší než grafika), aby rohy nezpůsobovaly falešné zásahy.
  • Výchozí rozměry se berou z bitmap.width/height, takže sprite musí mít nastavenou bitmap, než zavoláš hit/hit_point s výchozími rozměry; jinak předej explicitní hw/hh.
  • Hlídej si sám if sprite.visible (a jakýkoli příznak “naživu”) - pomocníci testují jen geometrii, ne to, jestli je entita aktivní. Viz /cs/memory/ o poolování uvolněných entit.