<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Blog Small Water</title>
    <link>https://smallwater.eu/cs/blog/</link>
    <description>Aplikace, webové nástroje a hry od malé softwarové firmy ze Slovenska.</description>
    <language>cs</language>
    <atom:link href="https://smallwater.eu/cs/blog/feed.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Mon, 09 Nov 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>Jak udělat LOD pro tisíce animovaných postav v Unity</title>
      <link>https://smallwater.eu/cs/blog/unity-lod-vertex-animation-crowd/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/unity-lod-vertex-animation-crowd/</guid>
      <pubDate>Mon, 09 Nov 2026 00:00:00 GMT</pubDate>
      <category>Tower Defense</category>
      <description>Návod na LOD pro dav v Unity s vertex animation textures: proč rozhodují vrcholy, ne trojúhelníky, LOD jako nová síť a výběr úrovně po entitách.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/cover.webp" /><img src="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/cover.jpg" alt="Tři verze téže postavy kostlivce s klesajícím počtem trojúhelníků zleva doprava" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#proc-generovany-lod-davu-nepomuze" data-ga="blog_toc_clicked" data-ga-heading-id="proc-generovany-lod-davu-nepomuze">Proč generovaný LOD davu nepomůže</a></li><li><a href="#pocitejte-vrcholy-ne-trojuhelniky" data-ga="blog_toc_clicked" data-ga-heading-id="pocitejte-vrcholy-ne-trojuhelniky">Počítejte vrcholy, ne trojúhelníky</a></li><li><a href="#krok-1-snizte-pocet-vrcholu-v-lod-0" data-ga="blog_toc_clicked" data-ga-heading-id="krok-1-snizte-pocet-vrcholu-v-lod-0">Krok 1: Snižte počet vrcholů v LOD 0</a></li><li><a href="#krok-2-lod-1-ze-zjednoduseni-se-sdilenou-texturou" data-ga="blog_toc_clicked" data-ga-heading-id="krok-2-lod-1-ze-zjednoduseni-se-sdilenou-texturou">Krok 2: LOD 1 ze zjednodušení se sdílenou texturou</a></li><li><a href="#krok-3-lod-2-jako-nova-sit-pripojena-k-originalu" data-ga="blog_toc_clicked" data-ga-heading-id="krok-3-lod-2-jako-nova-sit-pripojena-k-originalu">Krok 3: LOD 2 jako nová síť připojená k originálu</a></li><li><a href="#krok-4-diry-kontrolujte-strojove" data-ga="blog_toc_clicked" data-ga-heading-id="krok-4-diry-kontrolujte-strojove">Krok 4: Díry kontrolujte strojově</a></li><li><a href="#krok-5-vybirejte-uroven-pro-kazdou-postavu-zvlast" data-ga="blog_toc_clicked" data-ga-heading-id="krok-5-vybirejte-uroven-pro-kazdou-postavu-zvlast">Krok 5: Vybírejte úroveň pro každou postavu zvlášť</a></li><li><a href="#vysledky-a-limity" data-ga="blog_toc_clicked" data-ga-heading-id="vysledky-a-limity">Výsledky a limity</a></li></ol></nav><p>LOD pro tisíce animovaných postav v Unity stavte podle počtu vrcholů, ne trojúhelníků: u vertex animation textures se animace počítá na každý vrchol. Nižší úrovně připravte ručně, vrcholy nejnižší úrovně připojte k povrchu originálu a úroveň vybírejte pro každou postavu zvlášť. V naší tower defense hře pro PC to při 3 000 nepřátel zvýšilo FPS při záběru do davu z 62 na 120.</p>
<p>Postup se týká her, kde se na obrazovce pohybují tisíce postav najednou, tedy tower defense, strategií a survival her. Naše hra je postavená v Unity s ECS a najdete ji v <a href="https://smallwater.eu/cs/#referencie">referencích</a>. LOD znamená <a href="https://en.wikipedia.org/wiki/Level_of_detail_(computer_graphics)" target="_blank" rel="noopener">level of detail</a>: vzdálená postava se kreslí z jednodušší sítě. Obrázky jsou skutečné sítě našeho nepřítele kostlivce, ne záběry ze hry.</p>
<h2 id="proc-generovany-lod-davu-nepomuze"><a class="heading-anchor" href="#proc-generovany-lod-davu-nepomuze" aria-hidden="true" tabindex="-1">#</a>Proč generovaný LOD davu nepomůže</h2>
<p>Unity umí úrovně LOD vygenerovat při importu modelu (funkce <a href="https://docs.unity3d.com/6000.2/Documentation/Manual/lod/mesh-lod-introduction.html" target="_blank" rel="noopener">Mesh LOD</a>). Pro tisíce postav to má dva problémy.</p>
<p>První je vzhled. Automatické zjednodušení trhalo tenké končetiny a rozbíjelo UV, tedy mapování textury na síť.</p>
<p>Druhý je cena animace. Standardní kosterní animace jsou pro tisíce entit příliš drahé, proto se místo nich používají vertex animation textures (VAT). VAT je textura, kde každý sloupec patří jednomu vrcholu a každý řádek jednomu snímku animace. Shader si polohu vrcholu přečte z textury, takže cena animace závisí na počtu čtených vrcholů, a to i při kreslení stínů.</p>
<p>Horní hranici ukazuje test s vynucenou generovanou úrovní na všech postavách (Unity vygeneruje i úrovně 3 a 4, build, 1080p, 3 000 nepřátel, stíny zapnuté):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Vynucená úroveň</th>
<th>Trojúhelníky</th>
<th>FPS</th>
</tr>
</thead>
<tbody><tr>
<td>LOD 0</td>
<td>11 milionů</td>
<td>72</td>
</tr>
<tr>
<td>LOD 1</td>
<td>6 milionů</td>
<td>96</td>
</tr>
<tr>
<td>LOD 2</td>
<td>3,68 milionu</td>
<td>120</td>
</tr>
<tr>
<td>LOD 4</td>
<td>1,43 milionu</td>
<td>201</td>
</tr>
</tbody></table>
</div>
<p>Prostor pro zrychlení je velký. Je třeba ho získat bez viditelných děr a skoků mezi úrovněmi.</p>
<h2 id="pocitejte-vrcholy-ne-trojuhelniky"><a class="heading-anchor" href="#pocitejte-vrcholy-ne-trojuhelniky" aria-hidden="true" tabindex="-1">#</a>Počítejte vrcholy, ne trojúhelníky</h2>
<p>Poloviční počet trojúhelníků sám o sobě nepomůže. LOD s polovinou trojúhelníků a 996 z původních 1 521 vrcholů ušetřil 1,9 ms na snímek, LOD s 217 vrcholy 6,4 ms. U každé úrovně sledujte oba počty.</p>
<p>Vrcholů bývá zbytečně mnoho. Vrchol se zdvojí na každém švu UV, protože jedna poloha pak potřebuje dvě souřadnice textury. Naše modely z AI generátoru měly UV rozsekané na stovky ostrovů: kostlivec měl 1 521 vrcholů, ale jen 608 různých kombinací polohy a normály.</p>
<h2 id="krok-1-snizte-pocet-vrcholu-v-lod-0"><a class="heading-anchor" href="#krok-1-snizte-pocet-vrcholu-v-lod-0" aria-hidden="true" tabindex="-1">#</a>Krok 1: Snižte počet vrcholů v LOD 0</h2>
<p>První úspora nestojí žádný detail. Spojte vrcholy se shodnou polohou a podobnou normálou (rozdíl do 25°), rozbalte UV znovu se švy jen na ostrých hranách (nad 70°) a texturu přepočítejte z původní po trojúhelnících. Kostlivec klesl z 1 521 na 1 090 vrcholů, u ostatních typů nepřátel to bylo o 19 až 29 % méně, při stejném tvaru.</p>
<p>Do VAT pak zapisujte jen sloupce vrcholů, které některá úroveň skutečně používá. Ušetřilo to 40 až 60 % paměti animací.</p>
<h2 id="krok-2-lod-1-ze-zjednoduseni-se-sdilenou-texturou"><a class="heading-anchor" href="#krok-2-lod-1-ze-zjednoduseni-se-sdilenou-texturou" aria-hidden="true" tabindex="-1">#</a>Krok 2: LOD 1 ze zjednodušení se sdílenou texturou</h2>
<p>LOD 1 je zjednodušený LOD 0 na přibližně 52 % trojúhelníků. Zjednodušujte po kouscích (každá kombinace kosti a souvislé části sítě) a vrcholy na hranicích kousků zamkněte. Hlavě dejte větší rozpočet, protože silueta hlavy je to, co oko v davu rozezná.</p>
<p>Důležité je ponechat texturu LOD 0. Úroveň s vlastní texturou nebo jiným tvarem je při přechodu vidět i při podobné siluetě.</p>
<p>Pod zhruba 50 % trojúhelníků zjednodušení z existujících vrcholů trhá tenké části a samostatné kousky. Níž je třeba jít jinak.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/levels.webp" /><img src="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/levels.png" alt="Tři verze postavy kostlivce vedle sebe s klesajícím počtem trojúhelníků" width="2400" height="1240" loading="lazy" decoding="async" /></picture><figcaption>Tentýž kostlivec ve třech úrovních: 873, 448 a 330 trojúhelníků, 1 090, 705 a 520 vrcholů.</figcaption></figure>
<h2 id="krok-3-lod-2-jako-nova-sit-pripojena-k-originalu"><a class="heading-anchor" href="#krok-3-lod-2-jako-nova-sit-pripojena-k-originalu" aria-hidden="true" tabindex="-1">#</a>Krok 3: LOD 2 jako nová síť připojená k originálu</h2>
<p>Nejnižší úroveň je nová uzavřená síť. Originálu přidejte vrstvu tloušťky, proveďte voxel remesh a zjednodušte na cílový počet (u nás 200 až 460 trojúhelníků podle typu nepřítele). Výslednou síť obalte na povrch originálu a UV udělejte automaticky. Texturu zapečte z originálu ve stejné hustotě jako LOD 0, aby seděly i mipmapy.</p>
<p>Nová síť nemá váhy animace. Každý její vrchol proto připojte k nejbližšímu trojúhelníku originálu. Uložte si tři sloupce toho trojúhelníku a barycentrické váhy (podíly, které určují polohu bodu v trojúhelníku).</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/attach.webp" /><img src="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/attach.png" alt="Schéma: červený vrchol zjednodušené sítě je přerušovanými čarami připojen ke třem vrcholům původního trojúhelníku" width="1800" height="840" loading="lazy" decoding="async" /></picture><figcaption>Vrchol nižší úrovně (červený) se hýbe podle tří vrcholů nejbližšího trojúhelníku originálu.</figcaption></figure>
<p>Sloupec připojeného vrcholu ve VAT je vážený součet poloh a normál těch tří vrcholů v každém snímku. Shader nic neřeší, připojený vrchol je obyčejný další sloupec.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">pro každý snímek f:
    VAT[připojený, f] = w0 * VAT[a, f] + w1 * VAT[b, f] + w2 * VAT[c, f]</code></pre>
<h2 id="krok-4-diry-kontrolujte-strojove"><a class="heading-anchor" href="#krok-4-diry-kontrolujte-strojove" aria-hidden="true" tabindex="-1">#</a>Krok 4: Díry kontrolujte strojově</h2>
<p>Obrázky z modelovacího nástroje díry spolehlivě neukážou. Kontrola stačí jednoduchá: vykreslete originál a kandidáta z 11 směrů v rozlišení 120 px a porovnejte siluety.</p>
<p>Za díru počítejte pixely originálu uvnitř obrysu kandidáta, které kandidát nepokryje, a pixely kandidáta více než 2 px mimo obrys originálu. U nás je limit 1 % děr a 4 až 6 % ztráty. Kontrola musí respektovat odstraňování zadních stěn podle materiálu. Limity jsme nastavili podle toho, kde byly přechody ve hře okem vidět.</p>
<h2 id="krok-5-vybirejte-uroven-pro-kazdou-postavu-zvlast"><a class="heading-anchor" href="#krok-5-vybirejte-uroven-pro-kazdou-postavu-zvlast" aria-hidden="true" tabindex="-1">#</a>Krok 5: Vybírejte úroveň pro každou postavu zvlášť</h2>
<p>Přepínání úrovně pro celou mapu najednou je vidět. Když vybíráte po entitách, přechody se rozloží a v davu zaniknou.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Celá mapa najednou</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/select-global.webp" /><img src="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/select-global.png" alt="Dav teček v jedné barvě, všechny postavy mají najednou stejnou úroveň" width="1120" height="840" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Po entitách</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/select-entity.webp" /><img src="https://smallwater.eu/blog/unity-lod-vertex-animation-crowd/select-entity.png" alt="Dav teček v pásech podle vzdálenosti od kamery, blízké červené, vzdálené šedé" width="1120" height="840" loading="lazy" decoding="async" /></picture></div></div><figcaption>Pohled shora, kamera dole. Barva je úroveň LOD: červená 0, oranžová 1, šedá 2.</figcaption></figure>
<p>Upozornění k verzi: dokumentace Unity 6.2 uvádí, že Entities Graphics vždy používá LOD 0. Výběr po entitách podle vzorce níže používáme v Unity 6.6, před nasazením si ověřte chování vaší verze:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">LOD = log2(d * C / D) * sklon + posun
C = prah * 2 * tan(fov / 2) / vyska_obrazu</code></pre>
<p>Tady je <code>d</code> vzdálenost od kamery a <code>D</code> průměr světových hranic entity. Sklon a posun zvolte tak, aby úroveň 1 začínala na 93,3 a úroveň 2 na 167,9 metru (kamera s fov 30° a 1440 px). Globální práh LOD nastavte na výšku obrazu / 1440, potom vzdálenosti nezávisí na rozlišení.</p>
<p>Dvě chyby, na které si dejte pozor:</p>
<ul>
<li><strong>Měřítko.</strong> Průměr <code>D</code> počítejte ze světových hranic, tedy s měřítkem prefabu. Sítě pro VAT bývají v malých jednotkách a prefab je zvětší 2krát až 4krát. Bez toho vám hra celou dobu kreslí LOD 0 a nic nehlásí. Správnost ověřte počtem trojúhelníků v běžném režimu oproti vynucenému LOD. Pootočení entity zvětšuje osově zarovnané hranice, proto u nás průměr násobíme koeficientem 1,22.</li>
<li><strong>Bakování.</strong> Křivka výběru se zapéká při bakování SubScene a změnu v síti bakování nezachytí. Po její úpravě vynuťte nové bakování SubScene.</li>
</ul>
<h2 id="vysledky-a-limity"><a class="heading-anchor" href="#vysledky-a-limity" aria-hidden="true" tabindex="-1">#</a>Výsledky a limity</h2>
<p>Build pro macOS, 2560 × 1440, 3 000 nepřátel, stíny zapnuté, hra zastavená po prohře (animace VAT stále běží, takže měření platí pro grafiku):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Scéna</th>
<th>Před</th>
<th>Po</th>
</tr>
</thead>
<tbody><tr>
<td>Celá mapa</td>
<td>90 FPS</td>
<td>130 FPS</td>
</tr>
<tr>
<td>Přiblížení do davu</td>
<td>62 FPS</td>
<td>120 FPS</td>
</tr>
</tbody></table>
</div>
<p>Na <a href="https://www.steamdeck.com/en/tech" target="_blank" rel="noopener">Steam Decku</a> (1280 × 800, 8 výpočetních jednotek RDNA 2) je to 75 až 80 FPS na celé mapě. Nejbližší záběr do davu má při vysoké kvalitě stínů 33 FPS, bez stínů 64. Stíny jsou tedy největší zbývající položka: LOD snižuje i jejich cenu, ale vypnutí stále udělá velký skok.</p>
<p>Co LOD neřeší: kontrola děr běží v T-póze, takže natažení sítě v extrémních pózách neodhalí. Jeden typ nepřítele ještě limit děr v LOD 2 překračuje. Výpočet pohybu hordy LOD neovlivní, ten řeší článek o <a href="https://smallwater.eu/cs/blog/unity-dots-horde-navigation/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/unity-lod-vertex-animation-crowd/" data-ga-destination="/cs/blog/unity-dots-horde-navigation/" data-ga-placement="inline">navigaci hordy v Unity DOTS</a> a reakce na výbuchy <a href="https://smallwater.eu/cs/blog/fake-ragdoll-unity-dots/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/unity-lod-vertex-animation-crowd/" data-ga-destination="/cs/blog/fake-ragdoll-unity-dots/" data-ga-placement="inline">fake ragdoll</a>.</p>
<p>Potřebujete pomoct s výkonem hry v Unity nebo simulací davu? <a href="https://smallwater.eu/cs/#kontakt">Napište nám</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak přidat glow a 2D světla do staré 2D hry v Unity URP</title>
      <link>https://smallwater.eu/cs/blog/dreamica-remaster-unity-urp-glow/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/dreamica-remaster-unity-urp-glow/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Dreamica</category>
      <description>Jak v Unity URP dát staré 2D hře selektivní glow přes Bloom a HDR, dynamická 2D světla a ostré UI pro dnešní displeje. Postup na příkladu hry Dreamica.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/cover.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/cover.jpg" alt="Tři záběry ze hry Dreamica: duch pronásleduje bublinu, veverka s dělem a zářící lasery v lese" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#proc-nestaci-stary-celoobrazovkovy-glow" data-ga="blog_toc_clicked" data-ga-heading-id="proc-nestaci-stary-celoobrazovkovy-glow">Proč nestačí starý celoobrazovkový glow</a></li><li><a href="#krok-1-prepnete-projekt-na-urp" data-ga="blog_toc_clicked" data-ga-heading-id="krok-1-prepnete-projekt-na-urp">Krok 1: Přepněte projekt na URP</a><ol><li><a href="#seda-obrazovka-nastavte-zasobnik-kamer" data-ga="blog_toc_clicked" data-ga-heading-id="seda-obrazovka-nastavte-zasobnik-kamer">Šedá obrazovka: nastavte zásobník kamer</a></li><li><a href="#sprity-ignoruji-svetlo-prepnete-na-2d-renderer" data-ga="blog_toc_clicked" data-ga-heading-id="sprity-ignoruji-svetlo-prepnete-na-2d-renderer">Sprity ignorují světlo: přepněte na 2D renderer</a></li><li><a href="#castice-a-stare-pluginy" data-ga="blog_toc_clicked" data-ga-heading-id="castice-a-stare-pluginy">Částice a staré pluginy</a></li></ol></li><li><a href="#krok-2-selektivni-glow-pres-hdr-barvu-a-bloom-s-prahem-1" data-ga="blog_toc_clicked" data-ga-heading-id="krok-2-selektivni-glow-pres-hdr-barvu-a-bloom-s-prahem-1">Krok 2: Selektivní glow přes HDR barvu a Bloom s prahem 1</a><ol><li><a href="#emisni-mapa-zari-jen-cast-spritu" data-ga="blog_toc_clicked" data-ga-heading-id="emisni-mapa-zari-jen-cast-spritu">Emisní mapa: září jen část spritu</a></li><li><a href="#pulzovani-v-expozicnich-stupnich" data-ga="blog_toc_clicked" data-ga-heading-id="pulzovani-v-expozicnich-stupnich">Pulzování v expozičních stupních</a></li></ol></li><li><a href="#krok-3-dynamicke-2d-svetlo-kolem-hrace" data-ga="blog_toc_clicked" data-ga-heading-id="krok-3-dynamicke-2d-svetlo-kolem-hrace">Krok 3: Dynamické 2D světlo kolem hráče</a><ol><li><a href="#stejny-shader-na-navazujicich-plochach" data-ga="blog_toc_clicked" data-ga-heading-id="stejny-shader-na-navazujicich-plochach">Stejný shader na navazujících plochách</a></li><li><a href="#zablesk-prechod-na-vrchol-misto-pricteni" data-ga="blog_toc_clicked" data-ga-heading-id="zablesk-prechod-na-vrchol-misto-pricteni">Záblesk: přechod na vrchol místo přičtení</a></li></ol></li><li><a href="#krok-4-ostre-ui-pro-dnesni-displeje" data-ga="blog_toc_clicked" data-ga-heading-id="krok-4-ostre-ui-pro-dnesni-displeje">Krok 4: Ostré UI pro dnešní displeje</a></li><li><a href="#limity-nekomprimovane-textury" data-ga="blog_toc_clicked" data-ga-heading-id="limity-nekomprimovane-textury">Limity: nekomprimované textury</a></li></ol></nav><p>Stará 2D hra v Unity na vestavěném render pipeline vypadá na dnešním telefonu ploše: světlo chybí, glow buď není, nebo rozzáří celý obraz i s UI, a sprity rozhraní jsou rozmazané. V Unity URP to vyřešíte třemi věcmi: Bloomem s prahem 1 a HDR barvou jen na objektech, které mají zářit, 2D světly z 2D rendereru a UI s většími sprity, které respektuje bezpečnou oblast displeje.</p>
<p>Postup ukazujeme na naší hře Dreamica (<a href="https://smallwater.eu/dreamica-ios">App Store</a>, <a href="https://smallwater.eu/dreamica-android">Google Play</a>). Je to pádová adventura, ve které prstem kreslíte čáry a jimi vedete bublinu se spícím dítětem přes čtyři světy. Původně vznikla v Unity 5.3, UI bylo v pluginu NGUI, glow obstarával plugin MK Glow a grafika byla připravená pro iPhone 7 s displejem 1334 × 750 pixelů. Hratelnost zůstala stejná, změnila se jen grafika.</p>
<p>Starou hru často otevřete kvůli obchodu, ne kvůli grafice. Google Play od srpna 2019 <a href="https://android-developers.googleblog.com/2019/01/get-your-apps-ready-for-64-bit.html" target="_blank" rel="noopener">vyžaduje 64bitové verze</a> a hra, která má jen 32bitový build, z něj vypadne. Přesně tak dopadla i Dreamica. Když už projekt převádíte do Unity 6, grafika je poměrně levný bonus.</p>
<h2 id="proc-nestaci-stary-celoobrazovkovy-glow"><a class="heading-anchor" href="#proc-nestaci-stary-celoobrazovkovy-glow" aria-hidden="true" tabindex="-1">#</a>Proč nestačí starý celoobrazovkový glow</h2>
<p>Starší glow pluginy fungují jako celoobrazovkový efekt. Rozzáří všechno světlé včetně ikonek tlačítek v menu. V Dreamice ikonky pod MK Glow v Unity 6 skoro zmizely. Oficiální balík Post Processing Stack v2 je náhrada jen pro vestavěný pipeline, v URP se už neaplikuje. Pokud chcete glow, který řídíte po materiálech, a k tomu dynamická 2D světla, přejděte na URP a použijte Bloom, který je v něm zabudovaný.</p>
<h2 id="krok-1-prepnete-projekt-na-urp"><a class="heading-anchor" href="#krok-1-prepnete-projekt-na-urp" aria-hidden="true" tabindex="-1">#</a>Krok 1: Přepněte projekt na URP</h2>
<p>Po přepnutí na URP narazíte ve staré 2D hře téměř jistě na dvě pasti. Obě se projevují tak, že obraz vypadá špatně nebo přesně jako předtím, přestože shadery jsou v pořádku.</p>
<h3 id="seda-obrazovka-nastavte-zasobnik-kamer"><a class="heading-anchor" href="#seda-obrazovka-nastavte-zasobnik-kamer" aria-hidden="true" tabindex="-1">#</a>Šedá obrazovka: nastavte zásobník kamer</h3>
<p>Pokud herní okno v Editoru ukazuje jen šedou plochu, ale scéna v editoru i jednorázový render kamery vypadají správně, chyba není v shaderech, ale ve skládání kamer. Staré hry mívají dvě kamery, jednu na svět a jednu na UI, a UI kamera maže jen hloubku. Tak se vrství kamery ve vestavěném pipeline a URP to nepodporuje. Potřebuje explicitní zásobník kamer (camera stack): jedna kamera je základní (Base), ostatní jsou překryv (Overlay) zařazený do jejího zásobníku. Nastavte ho v každé scéně, kde se kamery vrství.</p>
<h3 id="sprity-ignoruji-svetlo-prepnete-na-2d-renderer"><a class="heading-anchor" href="#sprity-ignoruji-svetlo-prepnete-na-2d-renderer" aria-hidden="true" tabindex="-1">#</a>Sprity ignorují světlo: přepněte na 2D renderer</h3>
<p>Sprity s osvětleným shaderem nereagují na žádné světlo, pokud běží běžný URP renderer. Ten 2D světla vůbec nepočítá. Přepněte na 2D renderer. Ani ten ale osvětlení nekreslí, dokud ve scéně není alespoň jedno 2D světlo. Když něco po změně vypadá přesně jako předtím, nejdřív zkontrolujte, který renderer běží.</p>
<h3 id="castice-a-stare-pluginy"><a class="heading-anchor" href="#castice-a-stare-pluginy" aria-hidden="true" tabindex="-1">#</a>Částice a staré pluginy</h3>
<p>Částice na starých shaderech se pod 2D rendererem kreslí černě nebo vůbec. Dejte jim nový jednoduchý shader.</p>
<p>Pozor i na Asset Store. Při obnovení assetů umí Editor sám aplikovat „aktualizaci“ starého pluginu, která přidá metadata a přejmenuje shadery. Materiály hledají shader podle jména, takže se potichu rozbijí. Po každém větším obnovení assetů ve starém projektu si ve verzovacím systému projděte všechny změněné soubory, i ty, na kterých zrovna nepracujete.</p>
<h2 id="krok-2-selektivni-glow-pres-hdr-barvu-a-bloom-s-prahem-1"><a class="heading-anchor" href="#krok-2-selektivni-glow-pres-hdr-barvu-a-bloom-s-prahem-1" aria-hidden="true" tabindex="-1">#</a>Krok 2: Selektivní glow přes HDR barvu a Bloom s prahem 1</h2>
<p>Cílem je, aby zářily vybrané objekty, v Dreamice nakreslená čára, lasery, jiskry, světlušky a runy, ale ne pozadí a UI. Bloom v URP rozmaže a přičte všechno, co je jasnější než jeho <a href="https://docs.unity3d.com/6/Documentation/Manual/urp/post-processing-bloom.html" target="_blank" rel="noopener">práh</a>. Nastavte práh na 1 a materiálům, které mají zářit, vytlačte barvu nad 1.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">shader čáry:
    barva = textura × barva vrcholu × odstín × intenzita    (intenzita = 6)

Bloom:
    práh = 1, síla = 0,3
    září jen to, co má po výpočtu jas nad 1</code></pre>
<p>Pozadí a tlačítka mají barvy od 0 do 1, přes práh neprojdou a zůstanou ostrá. Tím zmizí i problém s vybělenými ikonkami. Podmínkou je kamera s HDR. Bez něj se barva ořízne na 1 a nezáří nic. 2D renderer přepínač HDR v inspektoru kamery neukazuje, proto ho zapněte skriptem.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Nízký práh, celý obraz</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/glow-fullscreen.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/glow-fullscreen.jpg" alt="Celoobrazovkový glow: září čára i okraj tlačítka a světlé části pozadí" width="1440" height="840" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Práh 1, HDR materiál</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/glow-selective.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/glow-selective.jpg" alt="Selektivní glow: září jen nakreslená čára, tlačítko a pozadí jsou ostré" width="1440" height="840" loading="lazy" decoding="async" /></picture></div></div><figcaption>Ilustrace principu. Vlevo celoobrazovkový glow s nízkým prahem, vpravo září jen materiál s intenzitou nad 1.</figcaption></figure>
<p>Když je glow takto pod kontrolou, nemusí být volitelný. V Dreamice jsme přepínač glow z nastavení zrušili, glow je součást vzhledu.</p>
<h3 id="emisni-mapa-zari-jen-cast-spritu"><a class="heading-anchor" href="#emisni-mapa-zari-jen-cast-spritu" aria-hidden="true" tabindex="-1">#</a>Emisní mapa: září jen část spritu</h3>
<p>Pokud má zářit jen část spritu, přidejte mu emisní mapu (emission map): druhou, černobílou texturu, která říká, která část spritu září. Jen ta část dostane barvu nad 1, takže u runy svítí jádro, ne celý obrys.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/rune-emission.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/rune-emission.png" alt="Runa, její černobílá emisní mapa a výsledek se zářícím jádrem" width="1800" height="600" loading="lazy" decoding="async" /></picture><figcaption>Sprite runy, emisní mapa (bílá září) a výsledek po Bloomu.</figcaption></figure>
<h3 id="pulzovani-v-expozicnich-stupnich"><a class="heading-anchor" href="#pulzovani-v-expozicnich-stupnich" aria-hidden="true" tabindex="-1">#</a>Pulzování v expozičních stupních</h3>
<p>Pokud má záření pulzovat, nenásobte barvu lineárně. HDR materiál může mít jas kolem 2 na osmou a lineární násobení od 0 do 10 na něm není vidět vůbec. Pulz počítejte v expozičních stupních (EV), stejně jako posuvník intenzity ve výběru HDR barvy v Unity:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">barva = odstín × 2 ^ ev,   ev kmitá mezi minimem a maximem</code></pre>
<h2 id="krok-3-dynamicke-2d-svetlo-kolem-hrace"><a class="heading-anchor" href="#krok-3-dynamicke-2d-svetlo-kolem-hrace" aria-hidden="true" tabindex="-1">#</a>Krok 3: Dynamické 2D světlo kolem hráče</h2>
<p>K hráči dejte bodové 2D světlo a kolem něj měkkou záři. V Dreamice jedna sinusovka řídí zároveň jas záře, poloměr světla i jeho intenzitu, takže pulzují spolu. Barvu světla může určovat prostředí: v lese je bílá, v bažině oranžová, v zimě fialová, v pekle žlutá. Pozadí a překážky přepněte na osvětlené shadery, jinak na světlo nereagují.</p>
<p>Krátká světla oživí události. Když vystřelí veverka s dělem, rozsvítí se jantarové světlo s intenzitou 3, které za 0,3 sekundy zhasne, a jiskry z hlavně září. Zářit nemusí všechno: prach při zmizení žaludu je záměrně hnědý a nezáří.</p>
<h3 id="stejny-shader-na-navazujicich-plochach"><a class="heading-anchor" href="#stejny-shader-na-navazujicich-plochach" aria-hidden="true" tabindex="-1">#</a>Stejný shader na navazujících plochách</h3>
<p>Pokud na sebe navazují dvě pozadí, zdrojové obrázky mají stejný jas, a přesto je mezi nimi vidět šev, zkontrolujte shadery. V Dreamice mělo úvodní pozadí a nekonečné pozadí lesa každé jiný shader. Jeden odstín materiálu 0,733 ignoroval, druhý ho použil a ztmavil les na 73 %. Řešením je stejný shader na obou plochách.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Dva shadery</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/seam-before.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/seam-before.jpg" alt="Hranice mezi dvěma pozadími lesa, spodní část je výrazně tmavší" width="1024" height="1120" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Jeden shader</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/seam-after.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/seam-after.jpg" alt="Stejná hranice, obě pozadí mají stejný jas" width="1024" height="1120" loading="lazy" decoding="async" /></picture></div></div><figcaption>Hranice mezi úvodním pozadím a lesem. Vlevo spodní část ztmavená odstínem 0,733, který horní shader ignoruje.</figcaption></figure>
<h3 id="zablesk-prechod-na-vrchol-misto-pricteni"><a class="heading-anchor" href="#zablesk-prechod-na-vrchol-misto-pricteni" aria-hidden="true" tabindex="-1">#</a>Záblesk: přechod na vrchol místo přičtení</h3>
<p>Při přechodu do dalšího světa světlo a záře vzplanou: náběh 0,6 sekundy, krátké podržení, doznění 1,4 sekundy a záře se přitom jednou otočí. Pokud záblesk jen přičtete k pulzu, vrchol závisí na tom, v jaké fázi je sinusovka zrovna v tu chvíli, a každý záblesk je jinak silný. Plynule přejděte z pulzu na pevný vrchol:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">váha = křivka záblesku (0 až 1)
hodnota = pulz + (vrchol - pulz) × váha</code></pre>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Přičtení</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/surge-additive.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/surge-additive.png" alt="Graf intenzity: pulz a dva záblesky s různě vysokým vrcholem" width="1440" height="720" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Přechod na vrchol</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/surge-lerp.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/surge-lerp.png" alt="Graf intenzity: pulz a dva záblesky se stejným vrcholem na tečkované čáře" width="1440" height="720" loading="lazy" decoding="async" /></picture></div></div><figcaption>Dva záblesky v různé fázi pulzu (oranžový a modrý). Vlevo se vrcholy liší, vpravo oba dosáhnou stejné hodnoty.</figcaption></figure>
<aside class="app-box app-box-callout"><img class="app-box-img" src="https://smallwater.eu/images/dreamica/1-chase.jpg" alt="" width="700" height="1515" loading="lazy" decoding="async" /><div class="app-box-body"><p class="app-box-eyebrow">Vyzkoušejte</p><h2 class="app-box-title">Dreamica</h2><p>Ručně kreslená pádová adventura, ve které kreslíš fyzikální čáry a vedeš magickou bublinu. Poznámky z vývoje a novinky.</p><div class="app-box-links"><a class="btn btn-primary" href="https://smallwater.eu/dreamica-ios" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="dreamica" data-ga-destination="/dreamica-ios" data-ga-app="dreamica" data-ga-store="app_store">App Store</a><a class="btn btn-ghost" href="https://smallwater.eu/dreamica-android" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="dreamica" data-ga-destination="/dreamica-android" data-ga-app="dreamica" data-ga-store="google_play">Google Play</a></div></div></aside>
<h2 id="krok-4-ostre-ui-pro-dnesni-displeje"><a class="heading-anchor" href="#krok-4-ostre-ui-pro-dnesni-displeje" aria-hidden="true" tabindex="-1">#</a>Krok 4: Ostré UI pro dnešní displeje</h2>
<p>UI kreslené pro iPhone 7 má malé sprity. Tlačítko v menu Dreamicy mělo sprite 128 × 64 pixelů a na telefonu s trojnásobnou hustotou bodů se roztahovalo na několikanásobek. Překreslete sprity ve větším rozlišení: nové tlačítko má 800 × 434, ukazatele postupu narostly z 200 × 26 na 800 × 104.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">128 × 64</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/button-old.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/button-old.jpg" alt="Detail rozmazaného okraje tlačítka" width="1120" height="608" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">800 × 434</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/button-new.webp" /><img src="https://smallwater.eu/blog/dreamica-remaster-unity-urp-glow/button-new.jpg" alt="Detail ostrého okraje tlačítka s viditelnou texturou" width="1120" height="608" loading="lazy" decoding="async" /></picture></div></div><figcaption>Stejné tlačítko při stejné velikosti na displeji. Vlevo původní sprite, vpravo nový.</figcaption></figure>
<p>Pokud UI stojí na NGUI, zvažte přepis. Kotvy v NGUI se dopočítávají po částech během několika snímků, dlouhé řetězy kotev se občas ustálí špatně a měřítko se liší mezi Editorem, telefonem a iPadem. V Dreamice to opravy spolehlivě nevyřešily, proto jsme UI přepsali na uGUI a TextMeshPro. Postupujte obrazovku po obrazovce a starou verzi mějte zapnutou vedle jako předlohu. UI škálujte podle výšky vůči referenčnímu rozlišení (u nás 434 × 941 bodů) a respektujte bezpečnou oblast displeje, tedy výřez a zaoblené rohy. Po přepisu NGUI z projektu úplně odstraňte, ať v něm nezůstanou dva systémy UI.</p>
<h2 id="limity-nekomprimovane-textury"><a class="heading-anchor" href="#limity-nekomprimovane-textury" aria-hidden="true" tabindex="-1">#</a>Limity: nekomprimované textury</h2>
<p>Tento postup mění jen grafiku. Textury jsme v Dreamice záměrně nekomprimovali, protože jsme chtěli plnou ostrost. Cenou je velikost: už původní hra měla v Editoru 313 MB textur v paměti a v App Store má 299 MB. Komprese by podle našeho odhadu zmenšila textury přibližně na 40 MB. Pokud vám na velikosti hry záleží, je to první místo, kde ušetřit.</p>
<p>Jiný příklad Unity hry od prvního řádku kódu po vydání najdete v článku o <a href="https://smallwater.eu/cs/blog/shmup-apple-watch-devlog/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/dreamica-remaster-unity-urp-glow/" data-ga-destination="/cs/blog/shmup-apple-watch-devlog/" data-ga-placement="inline">hře Space Impact Watch</a>, výkonu v Unity se věnuje článek o <a href="https://smallwater.eu/cs/blog/unity-dots-horde-navigation/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/dreamica-remaster-unity-urp-glow/" data-ga-destination="/cs/blog/unity-dots-horde-navigation/" data-ga-placement="inline">navigaci hordy v Unity DOTS</a>. Pokud je hra ještě v Unity 4, neotevírejte ji přímo v Unity 6; bezpečný postup popisuje návod <a href="https://smallwater.eu/cs/blog/unity-4-to-unity-6-upgrade/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/dreamica-remaster-unity-urp-glow/" data-ga-destination="/cs/blog/unity-4-to-unity-6-upgrade/" data-ga-placement="inline">jak převést projekt z Unity 4 do Unity 6</a>. Pokud máte starou Unity hru, která vypadla z obchodů, a chcete ji vrátit zpět, <a href="https://smallwater.eu/cs/#kontakt">ozvěte se nám</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak udělat fake ragdoll pro tisíce postav v Unity DOTS</title>
      <link>https://smallwater.eu/cs/blog/fake-ragdoll-unity-dots/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/fake-ragdoll-unity-dots/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Tower Defense</category>
      <description>Jak odhodit tisíce nepřátel výbuchem v Unity DOTS bez fyziky kostí: tělo jako jeden bod, deterministický odpal, dopad na průchozí buňku a rotace.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/cover.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/cover.jpg" alt="Postavy vymrštěné výbuchem letí po obloucích a otáčejí se ve vzduchu" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#proc-ne-skutecny-ragdoll" data-ga="blog_toc_clicked" data-ga-heading-id="proc-ne-skutecny-ragdoll">Proč ne skutečný ragdoll</a></li><li><a href="#telo-jako-jeden-bod" data-ga="blog_toc_clicked" data-ga-heading-id="telo-jako-jeden-bod">Tělo jako jeden bod</a></li><li><a href="#faze-srazeneho-nepritele" data-ga="blog_toc_clicked" data-ga-heading-id="faze-srazeneho-nepritele">Fáze sraženého nepřítele</a></li><li><a href="#odpal-vys-ne-dal" data-ga="blog_toc_clicked" data-ga-heading-id="odpal-vys-ne-dal">Odpal: výš, ne dál</a></li><li><a href="#dopad-na-pruchozi-bunku" data-ga="blog_toc_clicked" data-ga-heading-id="dopad-na-pruchozi-bunku">Dopad na průchozí buňku</a><ol><li><a href="#odraz-od-sten-ve-vzduchu-nestaci" data-ga="blog_toc_clicked" data-ga-heading-id="odraz-od-sten-ve-vzduchu-nestaci">Odraz od stěn ve vzduchu nestačí</a></li><li><a href="#predikce-drahy-pri-odpalu" data-ga="blog_toc_clicked" data-ga-heading-id="predikce-drahy-pri-odpalu">Predikce dráhy při odpalu</a></li></ol></li><li><a href="#rotace-naplanovana-podle-vysky-letu" data-ga="blog_toc_clicked" data-ga-heading-id="rotace-naplanovana-podle-vysky-letu">Rotace naplánovaná podle výšky letu</a></li><li><a href="#model-polozeny-na-zem" data-ga="blog_toc_clicked" data-ga-heading-id="model-polozeny-na-zem">Model položený na zem</a></li><li><a href="#mrtvi-nepratele-telo-decal-a-prach" data-ga="blog_toc_clicked" data-ga-heading-id="mrtvi-nepratele-telo-decal-a-prach">Mrtví nepřátelé: tělo, decal a prach</a></li><li><a href="#pasti-v-unity-entities" data-ga="blog_toc_clicked" data-ga-heading-id="pasti-v-unity-entities">Pasti v Unity Entities</a></li><li><a href="#vykon" data-ga="blog_toc_clicked" data-ga-heading-id="vykon">Výkon</a></li><li><a href="#limity-reseni" data-ga="blog_toc_clicked" data-ga-heading-id="limity-reseni">Limity řešení</a></li></ol></nav><p>Když výbuch ve hře odhodí stovky nepřátel najednou, skutečný ragdoll pro každého z nich je příliš drahý. Fake ragdoll v Unity DOTS uděláte tak, že celé tělo nepřítele simulujete jako jeden bod s vlastní jednoduchou fyzikou a model jen otáčíte kolem středu těla. Pád a vstávání jsou obyčejné animační klipy, kosti se nesimulují vůbec.</p>
<p>Postup ukazujeme na tower defense hře pro PC, kterou děláme v Unity s ECS (<a href="https://smallwater.eu/cs/#referencie">více v referencích</a>). Bomba nebo fireball tam vymršťuje desítky až stovky nepřátel najednou. Při 2 000 tělech ve vzduchu stojí simulace 0,53 až 0,59 ms na tik (tik je jeden krok herní simulace s pevnou délkou).</p>
<h2 id="proc-ne-skutecny-ragdoll"><a class="heading-anchor" href="#proc-ne-skutecny-ragdoll" aria-hidden="true" tabindex="-1">#</a>Proč ne skutečný ragdoll</h2>
<p>Skutečný ragdoll je deset a více fyzikálních těles spojených klouby na každou postavu. Při tisících postav na obrazovce to není reálné.</p>
<p>Druhý důvod je determinismus: ze stejného stavu musí vyjít stejný výsledek. V naší hře na tom stojí opakování prohrané vlny z kontrolního bodu a kontrolujeme to hashem stavu simulace po 1 800 ticích.</p>
<h2 id="telo-jako-jeden-bod"><a class="heading-anchor" href="#telo-jako-jeden-bod" aria-hidden="true" tabindex="-1">#</a>Tělo jako jeden bod</h2>
<p>Místo fyzikálního enginu použijte vlastní jednoduchou 2,5D fyziku: poloha na mřížce, výška nad zemí, balistický let, odraz od stěn a tření. V naší hře se takto pohybuje i bomba a nepřítel zasažený výbuchem se na chvíli stane stejným tělesem. Inspirací bylo, jak odlétají vojáci ve hře <a href="https://store.steampowered.com/app/1272320/Diplomacy_is_Not_an_Option/" target="_blank" rel="noopener">Diplomacy is Not an Option</a>.</p>
<p>Simulace zná jen polohu, výšku a úhel natočení. Otáčení modelu do ní nezapisujte. Patří do dodatečné transformační matice entity, kterou balík Entities (ECS v Unity) podporuje pro nerovnoměrné měřítko (<a href="https://docs.unity3d.com/Packages/com.unity.entities@1.3/api/Unity.Transforms.PostTransformMatrix.html" target="_blank" rel="noopener">dokumentace</a>).</p>
<h2 id="faze-srazeneho-nepritele"><a class="heading-anchor" href="#faze-srazeneho-nepritele" aria-hidden="true" tabindex="-1">#</a>Fáze sraženého nepřítele</h2>
<p>Sražený nepřítel projde třemi fázemi a pak se vrátí do davu:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">při zásahu:
    spočítej rychlost odpalu (síla, vzdálenost od výbuchu, náhoda ze seedu)
    najdi dopad na průchozí buňku (viz níže)
    zapni stav &quot;sražený&quot;, uvolni místo, které měl obsazené u cíle útoku
let:       těleso letí po parabole, model se otáčí kolem středu těla
dopad:     poloha podle natočení (záda nebo břicho), krátké doklopení
leh:       čeká pevný počet tiků
vstávání:  klip vstávání ze zad nebo z břicha, pak zpět do davu</code></pre>
<p>Stav „sražený“ udělejte jako komponentu, kterou lze zapnout a vypnout bez přesouvání entity mezi chunky (paměťovými bloky, ve kterých ECS drží entity se stejnou sadou komponent). Navigace, odtlačování v davu i rozhodování o útoku pak sražené jednoduše přeskočí. Jak funguje navigace a odtlačování velkého davu, popisuje článek o <a href="https://smallwater.eu/cs/blog/unity-dots-horde-navigation/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/fake-ragdoll-unity-dots/" data-ga-destination="/cs/blog/unity-dots-horde-navigation/" data-ga-placement="inline">navigaci hordy v Unity DOTS</a>.</p>
<h2 id="odpal-vys-ne-dal"><a class="heading-anchor" href="#odpal-vys-ne-dal" aria-hidden="true" tabindex="-1">#</a>Odpal: výš, ne dál</h2>
<p>Nejjednodušší odpal je pod úhlem 45° ± 10°. Při síle bomby 18 m/s a gravitaci 20 m/s² vychází vrchol letu kolem 4 m. Tělo přeletí nízko nad davem a efekt se v něm ztratí.</p>
<p>Vyšší síla nepomůže, protože s ní roste i dolet. Rozdělte rychlost na dvě složky a každou násobte zvlášť: úhel 60° ± 5°, vodorovná složka × 0,85, svislá × 1,3. Vrchol je pak kolem 10 m, dolet zůstane kolem 15,5 m a let trvá přibližně 2 sekundy.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/launch-arcs.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/launch-arcs.png" alt="Boční pohled: nízký přerušovaný oblouk a vysoký oblouk přes dům se siluetami postavy, která se během letu otáčí" width="2200" height="1520" loading="lazy" decoding="async" /></picture><figcaption>Přerušovaně odpal pod 45°, plnou čarou odpal pod 60° s oddělenými násobky. Dům má 5 m.</figcaption></figure>
<p>Směr se od výbuchu odchyluje o ± 15°, síla o ± 20 % a na okraji výbuchu klesá na polovinu. Náhodu neberte z globálního generátoru, ale z hashe seedu levelu, identifikátoru nepřítele a pořadového čísla výbuchu. Požadavky na sražení zpracovávejte seřazené, protože joby běžící na více vláknech je vytvoří v libovolném pořadí. Dva běhy téže vlny tak dají stejné oblouky.</p>
<p>Odhození je ano, nebo ne. Každý sražitelný nepřítel při zásahu aspoň upadne (rychlost je zdola omezená na 2 m/s), odolnost mění jen dolet. Imunitu dejte samostatným příznakem, v naší hře ho má největší typ nepřítele. Kdyby imunita vyplývala z odolnosti (při 0,95 vychází rychlost pod 2 m/s) a slabé odhození by se rušilo, byl by nepřítel imunní jen náhodou.</p>
<h2 id="dopad-na-pruchozi-bunku"><a class="heading-anchor" href="#dopad-na-pruchozi-bunku" aria-hidden="true" tabindex="-1">#</a>Dopad na průchozí buňku</h2>
<p>Nepřítel nesmí dopadnout do rokle, na útes ani do půdorysu věže. Průchodnost se určuje po buňkách mřížky a pro každou velikostní třídu nepřítele zvlášť.</p>
<h3 id="odraz-od-sten-ve-vzduchu-nestaci"><a class="heading-anchor" href="#odraz-od-sten-ve-vzduchu-nestaci" aria-hidden="true" tabindex="-1">#</a>Odraz od stěn ve vzduchu nestačí</h3>
<p>První, co napadne, je brát neprůchozí buňky jako stěny, od kterých se letící tělo odrazí. U útesu to vypadá přirozeně, u rokle ne: nad roklí je vzduch, ale buňka je neprůchozí, takže se tělo odrazí od neviditelného sloupu nad jejím okrajem. Navíc nic nezaručuje, že po odrazu skončí na dobrém místě.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/dead-end-bounce.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/dead-end-bounce.png" alt="Boční pohled: tělo letí přes rokli, uprostřed letu se odrazí od neviditelného sloupu nad jejím okrajem a spadne zpět" width="2200" height="1640" loading="lazy" decoding="async" /></picture><figcaption>Odraz od neprůchozí buňky. Tělo se odrazí od vzduchu nad okrajem rokle.</figcaption></figure>
<h3 id="predikce-drahy-pri-odpalu"><a class="heading-anchor" href="#predikce-drahy-pri-odpalu" aria-hidden="true" tabindex="-1">#</a>Predikce dráhy při odpalu</h3>
<p>Spolehlivější je zjistit dopad hned při odpalu. Let nasimulujte dopředu stejným krokem jako skutečnou fyziku a zkontrolujte buňku dopadu pro velikostní třídu nepřítele. Pokud není průchozí, zkoušejte další varianty v pevném pořadí:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">pro úhel v [0, +25, -25, +50, -50, ... , +175, -175]:
    pro násobek vodorovné rychlosti v [1, 0.7, 0.4, 0.1]:
        nasimuluj let
        pokud je buňka dopadu průchozí pro třídu nepřítele: použij tento směr a konec
pokud nic nevyšlo: vodorovná rychlost = 0 (výskok na místě)</code></pre>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/fit-search.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/fit-search.png" alt="Pohled shora na mřížku s roklí a vyvýšeným útesem, přeškrtnuté zamítnuté dopady a zvýrazněný první průchozí" width="2200" height="1400" loading="lazy" decoding="async" /></picture><figcaption>Výbuch, nepřítel, zamítnuté dopady a vybraný. Šedé body jsou všechny kandidátní dopady.</figcaption></figure>
<p>Nejhorší případ je 60 simulací letu na jednoho nepřítele (15 směrů × 4 násobky). Svislá rychlost se nemění, takže výška letu zůstává stejná a zkracuje se jen dolet. Pokud nepřítele během letu zasáhne další výbuch, spusťte predikci znovu.</p>
<figure class="post-figure post-figure-video"><video class="post-video" muted loop playsinline controls preload="none" poster="https://smallwater.eu/blog/fake-ragdoll-unity-dots/ragdoll-loop.jpg" width="1280" height="720" data-lazy-video aria-label="Ilustrace ve 2D: dva nepřátelé by dopadli do rokle (křížky), predikce jim zkrátí let."><source src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/ragdoll-loop.mp4" type="video/mp4" /></video><figcaption>Ilustrace ve 2D: dva nepřátelé by dopadli do rokle (křížky), predikce jim zkrátí let.</figcaption></figure>
<p>Aby tělo po dopadu neklouzalo ani neodskočilo, nastavte mu koeficient odrazu od země 0 a velké tření. Zůstane tam, kde predikce řekla.</p>
<h2 id="rotace-naplanovana-podle-vysky-letu"><a class="heading-anchor" href="#rotace-naplanovana-podle-vysky-letu" aria-hidden="true" tabindex="-1">#</a>Rotace naplánovaná podle výšky letu</h2>
<p>Nejjednodušší je točit model náhodnou rychlostí, například 180 až 540 °/s, a po dopadu ho za 0,2 s doklopit do nejbližšího lehu. Doklopení je ale vidět, když tělo dopadne téměř vzpřímeně. Dorovnávat rychlost otáčení v každém tiku tak, aby úhel při dopadu vyšel přesně vodorovně, pomůže jen částečně: i nízký odhoz se točí jako salto a otáčení nesouvisí s tím, jak vysoko tělo letí.</p>
<p>Lepší je naplánovat rotaci při odpalu z výšky letu:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">výška = vz² / (2 g), čas letu z vz a výšky
cíl = nejbližší vodorovná poloha v náhodném směru (čtvrt otáčky ze stoje)
cíl += půlotáčka za každých 2,5 m výšky, nejvýš 3
rychlost otáčení = (cíl - aktuální úhel) / čas letu</code></pre>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/tumble-plan.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/tumble-plan.png" alt="Dva oblouky: nízký odhoz, při kterém se postava jen převalí, a vysoký, při kterém udělá tři a čtvrt otáčky" width="2200" height="1200" loading="lazy" decoding="async" /></picture><figcaption>Nízký odhoz (vrchol kolem 2 m) udělá 90°, bomba (kolem 10 m) 90° a tři půlotáčky.</figcaption></figure>
<p>Strana dopadu vyplývá z úhlu: obličejem k zemi znamená břicho, jinak záda. Podle ní se vybere klip vstávání. Ležící nebo vstávající nepřítel si při dalším zásahu ponechá aktuální úhel a neskočí zpět do vzpřímené polohy.</p>
<h2 id="model-polozeny-na-zem"><a class="heading-anchor" href="#model-polozeny-na-zem" aria-hidden="true" tabindex="-1">#</a>Model položený na zem</h2>
<p>Model se otáčí kolem středu těla, ale země je pod jeho nejnižším bodem a ten se při otáčení mění. Odhad posunu ze dvou konstant a sinu úhlu nesedí pro modely s rozhozenýma rukama v klipu pádu: v naší hře se model kostlivce (běžný nepřítel) zanořoval až 1,15 m pod zem a model nekromanta až 1,73 m.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Odhad</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/ground-before.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/ground-before.png" alt="Šest natočení postavy, části pod zemí jsou červené" width="2200" height="840" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Změřeno</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/ground-after.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/ground-after.png" alt="Šest natočení postavy, každé se dotýká země nejnižším bodem" width="2200" height="840" loading="lazy" decoding="async" /></picture></div></div><figcaption>Stejná natočení s posunem z konstant a ze změřené tabulky.</figcaption></figure>
<p>Spolehlivější je nejnižší bod změřit. Při zapékání (bake) animací spočítejte pro klip pádu nejnižší bod těla při 72 natočeních po 5° a za běhu z tabulky interpolujte. Nejhlubší zanoření tak klesne na 4 mm. Jedno minimum přes všechny snímky klipu ale nestačí: tělo by po dopadu viselo 0,15 až 0,28 m nad zemí, dokud ho klip vstávání nestáhne dolů. Tabulka proto musí mít rozměr snímky × natočení.</p>
<p>Při pomalé naplánované rotaci se ukáže ještě jeden efekt. Pokud model pokládáte nejnižším bodem na výšku letu, střed těla při přechodu ze stoje do vodorovné polohy klesne o rozdíl poloviny výšky a poloviny tloušťky postavy. Tělo pak na začátku letu stoupá jen pomalu a teprve potom zrychlí. Model proto zvedněte o rozdíl polovičního rozměru ve stoje a při aktuálním úhlu, nejvýš o výšku letu. Střed těla pak jde po parabole.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/flight-lift.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/flight-lift.png" alt="Graf výšky středu těla v čase: přerušovaná ideální parabola, červená křivka s poklesem a modrá, která parabolu sleduje" width="2200" height="1040" loading="lazy" decoding="async" /></picture><figcaption>Výška středu těla během letu: bez zdvihu (červená) a se zdvihem (modrá).</figcaption></figure>
<h2 id="mrtvi-nepratele-telo-decal-a-prach"><a class="heading-anchor" href="#mrtvi-nepratele-telo-decal-a-prach" aria-hidden="true" tabindex="-1">#</a>Mrtví nepřátelé: tělo, decal a prach</h2>
<p>Nepřítel zabitý výbuchem má odletět také. Vytvořte pro něj letící tělo: kopii entity bez herních komponent (životy, odměna, cíl). Tělo přeletí, dopadne, chvíli leží a zmizí. Na jeho místě zůstane decal mrtvoly (textura promítnutá na zem) jako ve Warcraftu 3 a obláček prachu. V naší hře je strop 1 000 letících těl a 1 500 decalů.</p>
<p>Decal je pohled shora na snímek z klipu pádu, natočený stejně jako ležící tělo, takže výměna těla za decal není vidět. První snímek klipu vstávání se nehodí, protože postavu ze zad už posadí. Obláčky prachu slučujte v buňce 1,5 m. Bomba do davu tak vytvoří pár větších mraků místo stovek malých.</p>
<h2 id="pasti-v-unity-entities"><a class="heading-anchor" href="#pasti-v-unity-entities" aria-hidden="true" tabindex="-1">#</a>Pasti v Unity Entities</h2>
<ul>
<li><strong>Matice z bakeru zmizí.</strong> Pokud dodatečnou matici přidáte při bake, transformační systém ji při rovnoměrném měřítku odstraní a sražení se nehýbou. Při bake si proto vyžádejte nerovnoměrné měřítko. Tehdy se ale do stejné matice přesune celé měřítko prefabu, takže ho nepřepisujte samotnou rotací. Jinak má sražený nepřítel se zvětšením 2× poloviční velikost, i když vstane. Měřítko si uložte zvlášť a matici skládejte z měřítka a rotace.</li>
<li><strong>Výška kořene při importu klipů.</strong> U humanoid klipů se svislý pohyb kořene (root) zahodí a postavy při vstávání propadnou pod podlahu. Výšku kořene zapečte do pózy. Volbu ukotvit pózu na chodidla nepoužívejte, posune celé tělo o metr dolů.</li>
<li><strong>Matice v uzavřeném tvaru.</strong> Posun, rotace a zpětný posun jako tři násobení matic jsou v jobu měřitelně pomalejší než ručně napsaný vzorec pro výslednou matici.</li>
<li><strong>Burst se starou verzí.</strong> Po změně polí komponenty, kterou čte Burst job, může editor padat nebo hlásit nesmysly. Pomůže restart editoru.</li>
</ul>
<h2 id="vykon"><a class="heading-anchor" href="#vykon" aria-hidden="true" tabindex="-1">#</a>Výkon</h2>
<p>Měřeno v Unity Editoru na M3 Pro, 10 000 kostlivců v benchmark scéně, síla odpalu 18. Časy jsou součet CPU času Burst jobů přes všechna vlákna, ne čas snímku. Graf a tabulka jsou bez predikce dráhy a tabulek dopadu.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/fake-ragdoll-unity-dots/bench.webp" /><img src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/bench.png" alt="Graf času na tik podle počtu sražených nepřátel pro simulaci a přípravu vizuálu, s cílovou čarou 0,5 ms" width="2200" height="1120" loading="lazy" decoding="async" /></picture><figcaption>Simulace a příprava vizuálu bez predikce dráhy. Cíl 0,5 ms při 2 000 sražených.</figcaption></figure>
<div class="table-wrap"><table>
<thead>
<tr>
<th>sražených</th>
<th>simulace (ms/tik)</th>
<th>příprava vizuálu (ms)</th>
</tr>
</thead>
<tbody><tr>
<td>500</td>
<td>0,28 až 0,30</td>
<td>0,15</td>
</tr>
<tr>
<td>2 000</td>
<td>0,53</td>
<td>0,30 až 0,55</td>
</tr>
<tr>
<td>5 000</td>
<td>0,74 až 0,75</td>
<td>0,49 až 0,91</td>
</tr>
<tr>
<td>10 000</td>
<td>0,96 až 1,01</td>
<td>0,68 až 1,24</td>
</tr>
</tbody></table>
</div>
<ul>
<li>Simulace roste lineárně, zhruba o 0,25 ms na tisíc sražených. S predikcí dráhy a tabulkami dopadu je to při 2 000 sražených 0,58 až 0,59 ms, tedy mírně nad cílem 0,5 ms. Rozumný strop je proto 3 000 současně sražených, což je kolem 0,8 ms.</li>
<li>Výbuch do skupiny 359 nepřátel u stěny stojí v systému požadavků na sražení 1,3 až 1,9 ms v jednom snímku. Většinu tvoří predikce dráhy.</li>
<li>FPS z editoru se porovnat nedá, mezi dvěma běhy stejné konfigurace byl rozdíl až 80 FPS.</li>
</ul>
<h2 id="limity-reseni"><a class="heading-anchor" href="#limity-reseni" aria-hidden="true" tabindex="-1">#</a>Limity řešení</h2>
<p>Příprava vizuálu stoupla v jiné testovací scéně až na 1,3 ms, pravděpodobně kvůli čtení tabulky dopadu pro každé tělo. Je to první kandidát na optimalizaci. FPS v buildu a zátěž při hromadném zabití 1 000 nepřátel najednou (cíl do 1,5 ms navíc na snímek) zatím nemáme změřené.</p>
<p>Pokud řešíte podobnou simulaci davu nebo efektů v Unity a potřebujete pomoc, <a href="https://smallwater.eu/cs/#kontakt">napište nám</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak naprogramovat shmup pro Apple Watch</title>
      <link>https://smallwater.eu/cs/blog/shmup-apple-watch-devlog/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/shmup-apple-watch-devlog/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Space Impact Watch</category>
      <description>Návod na shmup pro Apple Watch: virtuální rozlišení, ovládání korunkou, kolize obdélníky, pooling, levely jako tabulka časů a generátor vln.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/cover.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/cover.jpg" alt="Pixelová scéna na zeleném displeji: loď vlevo střílí na komety, rakety a bosse vpravo" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#pevne-virtualni-rozliseni-84-48" data-ga="blog_toc_clicked" data-ga-heading-id="pevne-virtualni-rozliseni-84-48">Pevné virtuální rozlišení 84 × 48</a></li><li><a href="#ovladani-korunkou-a-automaticka-strelba" data-ga="blog_toc_clicked" data-ga-heading-id="ovladani-korunkou-a-automaticka-strelba">Ovládání korunkou a automatická střelba</a></li><li><a href="#herni-smycka-v-jedne-scene" data-ga="blog_toc_clicked" data-ga-heading-id="herni-smycka-v-jedne-scene">Herní smyčka v jedné scéně</a></li><li><a href="#kolize-obdelniky-misto-fyzikalniho-enginu" data-ga="blog_toc_clicked" data-ga-heading-id="kolize-obdelniky-misto-fyzikalniho-enginu">Kolize obdélníky místo fyzikálního enginu</a></li><li><a href="#pooling-jen-tam-kde-se-vyplati" data-ga="blog_toc_clicked" data-ga-heading-id="pooling-jen-tam-kde-se-vyplati">Pooling jen tam, kde se vyplatí</a></li><li><a href="#levely-jako-tabulka-casu" data-ga="blog_toc_clicked" data-ga-heading-id="levely-jako-tabulka-casu">Levely jako tabulka časů</a></li><li><a href="#nekonecny-rezim-z-jednoho-pseudonahodneho-cisla" data-ga="blog_toc_clicked" data-ga-heading-id="nekonecny-rezim-z-jednoho-pseudonahodneho-cisla">Nekonečný režim z jednoho pseudonáhodného čísla</a></li><li><a href="#limity-tohoto-pristupu" data-ga="blog_toc_clicked" data-ga-heading-id="limity-tohoto-pristupu">Limity tohoto přístupu</a></li></ol></nav><p>Shmup (shoot &#39;em up, střílečka, ve které loď letí proti vlnám nepřátel) na Apple Watch naráží na tři věci: malý displej, prst, který zakryje polovinu hry, a různé velikosti hodinek. Řeší se třemi rozhodnutími: celá hra běží v pevné mřížce 84 × 48 bodů, kolize se počítají ručně obdélníky a levely jsou tabulky s časy, ne kód. Loď se ovládá korunkou Digital Crown a střílí sama.</p>
<p>Postup ukazujeme na hře <a href="https://smallwater.eu/space-impact-watch">Space Impact Watch</a>, remaku střílečky Space Impact z telefonu Nokia 3310 z roku 2000, která běží na hodinkách i na iPhonu a iPadu. Příklady jsou ze SpriteKitu, 2D herního frameworku od Applu, ale principy platí pro každý shmup na malou obrazovku.</p>
<h2 id="pevne-virtualni-rozliseni-84-48"><a class="heading-anchor" href="#pevne-virtualni-rozliseni-84-48" aria-hidden="true" tabindex="-1">#</a>Pevné virtuální rozlišení 84 × 48</h2>
<p>Nejprve se rozhodněte, v jakých jednotkách bude hra počítat. Pokud použijete body obrazovky, svět bude na každé velikosti hodinek jinak velký. Lepší je zvolit malé virtuální rozlišení a všechno počítat v něm. V naší hře je to 84 × 48, přesně jako měl <a href="https://www.adafruit.com/products/338" target="_blank" rel="noopener">displej Nokie 3310</a>. Pozice, rychlosti, velikosti spritů i dráhy nepřátel jsou v těchto jednotkách. Při startu scény se jednou spočítá, kolik bodů obrazovky má jeden virtuální pixel (šířka scény děleno 84), a vše se jím jen vynásobí.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/grid.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/grid.png" alt="Herní scéna s viditelnou mřížkou 84 × 48 virtuálních pixelů" width="1008" height="576" loading="lazy" decoding="async" /></picture><figcaption>Level na mřížce 84 × 48. Každý čtvereček je jeden virtuální pixel, hodinky ho vykreslí jako blok několika skutečných bodů.</figcaption></figure>
<p>Co tím získáte:</p>
<ul>
<li><strong>Stejný vzhled na každém zařízení.</strong> Hra vypadá stejně na každé velikosti hodinek i na iPadu, mění se jen velikost bloku.</li>
<li><strong>Level design v malých celých číslech.</strong> Nepřítel letí ve výšce 13, raketa zatáčí v bodě x = 20.</li>
<li><strong>Jednoduchou grafiku.</strong> Sprity jsou jednobarevné a kreslené v desetinásobné velikosti, jeden virtuální pixel je ve zdrojovém obrázku blok 10 × 10. Barvu dostanou až ve hře. Na světlých levelech jsou černé na zeleném pozadí, na tmavých je to obráceně.</li>
</ul>
<h2 id="ovladani-korunkou-a-automaticka-strelba"><a class="heading-anchor" href="#ovladani-korunkou-a-automaticka-strelba" aria-hidden="true" tabindex="-1">#</a>Ovládání korunkou a automatická střelba</h2>
<p>Na hodinkách prst zakryje polovinu hry, proto se loď neovládá dotykem. Pohyb řeší Digital Crown: otočení korunky posune loď nahoru nebo dolů. Přidejte nastavení citlivosti a možnost otočit směr.</p>
<p>Střelba je automatická, v naší hře každých 0,4 sekundy, od čtvrtého levelu každých 0,3. Displej slouží jen pro jedno gesto: přetažením doprava hráč použije jednu ze tří speciálních zbraní.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/weapons.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/weapons.png" alt="Tři speciální zbraně: svislá čára, naváděná raketa a laser zasahující bosse" width="1104" height="240" loading="lazy" decoding="async" /></picture><figcaption>Tři speciální zbraně: čára, která projde celou výškou plochy, naváděná raketa a laser.</figcaption></figure>
<p>Na hodinkách je vibrace při zásahu stejně důležitá jako animace, malý výbuch na displeji si hráč snadno nevšimne. Haptiku volejte přes jedno rozhraní (zásah, smrt nepřítele, poražený boss), které každá platforma provede po svém: hodinky vibrací, iPhone vlastním způsobem. Verze pro telefon nemá korunku, místo ní má tlačítka na displeji, která používají stejné nastavení citlivosti i směru.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/watch-2021.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/watch-2021.png" alt="Screenshot hry v simulátoru Apple Watch, loď proti bossovi" width="368" height="448" loading="lazy" decoding="async" /></picture><figcaption>Hra v simulátoru Apple Watch: loď proti bossovi.</figcaption></figure>
<h2 id="herni-smycka-v-jedne-scene"><a class="heading-anchor" href="#herni-smycka-v-jedne-scene" aria-hidden="true" tabindex="-1">#</a>Herní smyčka v jedné scéně</h2>
<p>Celá hra může být jedna scéna s jednou smyčkou:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">každý snímek:
    posuň všechny objekty
    zkontroluj kolize
    podle času v levelu přidej nové nepřátele</code></pre>
<p>Věci přidávejte v pořadí, ve kterém se dají co nejdříve testovat:</p>
<ul>
<li><strong>Střelba a pohyb hráče.</strong> Bez nich se nedá nic otestovat.</li>
<li><strong>Nepřátelé a kolize.</strong> Prvnímu nepříteli stačí letět rovně doleva a zemřít po jednom zásahu.</li>
<li><strong>Vibrace a animace zásahu.</strong> Na malém displeji bez nich hráč neví, že zasáhl.</li>
<li><strong>Pozadí a HUD.</strong> Životy, skóre, počet speciálních zbraní. Pokud HUD v některém levelu překáží, přesuňte ho dolů.</li>
</ul>
<p>Na konci máte hratelnou smyčku se životy, skóre, výbuchy, pozadím a respawnem hráče. Pokud herní logiku držíte odděleně od vykreslování a vstupu, mohou ji sdílet verze pro hodinky i telefon.</p>
<h2 id="kolize-obdelniky-misto-fyzikalniho-enginu"><a class="heading-anchor" href="#kolize-obdelniky-misto-fyzikalniho-enginu" aria-hidden="true" tabindex="-1">#</a>Kolize obdélníky místo fyzikálního enginu</h2>
<p>SpriteKit má fyzikální engine a nabízí se použít ho i na kolize. Každý objekt dostane fyzikální tělo vytvořené z obrysu spritu, přesnou detekci kolizí a kategorii (hráč, nepřítel, střela, speciální zbraň) a engine sám hlásí dvojice, které se dotkly. Apple v <a href="https://developer.apple.com/documentation/spritekit/getting-started-with-physics-bodies" target="_blank" rel="noopener">dokumentaci k fyzikálním tělům</a> uvádí, že těla z alfa kanálu textury jsou nejpřesnější a zároveň výpočetně nejdražší. V naší hře jsme to zkusili a vrátili jsme se k ruční kontrole.</p>
<p>Ruční kontrola je jedna funkce, která postupně projde dvojice skupin a porovná obdélníky:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">pro každou dvojici skupin:
    (hráč a nepřátelé, hráč a střely, střely a nepřátelé, střely a střely)
    pro každý objekt A z první skupiny a objekt B z druhé:
        pokud se obdélník A překrývá s obdélníkem B:
            vyřeš zásah</code></pre>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/hitbox.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/hitbox.png" alt="Dvě dvojice spritů s vyznačenými obdélníky kolize, vlevo se překrývají jen rohy obdélníků" width="960" height="352" loading="lazy" decoding="async" /></picture><figcaption>Vlevo: obdélníky se překrývají, pixely ne, hra to počítá jako zásah. Vpravo: skutečný zásah.</figcaption></figure>
<p>Výhody: pořadí kontrol máte pod kontrolou a celé pravidlo „co se stane při zásahu“ je na jednom místě. Nepřesnost je malá, protože na mřížce 84 × 48 jsou sprity tak drobné, že obdélník a skutečný obrys se liší jen o pár virtuálních pixelů.</p>
<h2 id="pooling-jen-tam-kde-se-vyplati"><a class="heading-anchor" href="#pooling-jen-tam-kde-se-vyplati" aria-hidden="true" tabindex="-1">#</a>Pooling jen tam, kde se vyplatí</h2>
<p>Pooling znamená, že objekt po zániku nesmažete, jen ho skryjete a příště použijete znovu. Je lákavé poolovat všechno včetně výbuchů a power-upů. Má to ale cenu: každá kolize a každý update musí nejdřív ověřit, jestli je objekt vůbec živý. Stačí jedna zapomenutá kontrola a skrytý nepřítel zabije hráče nebo skrytá střela sestřelí raketu. Když jsme v naší hře pooling ze všeho odstraňovali, mazali jsme hlavně tyto kontroly.</p>
<p>Jednoduché pravidlo: poolujte to, čeho je hodně a vzniká to pořád stejně. Hráč si při startu vytvoří 21 střel a pak je jen recykluje. Nepřátelé a jejich střely se také znovu používají, výbuchy a power-upy vznikají a zanikají normálně.</p>
<h2 id="levely-jako-tabulka-casu"><a class="heading-anchor" href="#levely-jako-tabulka-casu" aria-hidden="true" tabindex="-1">#</a>Levely jako tabulka časů</h2>
<p>Level nemusí být kód, stačí seznam záznamů. Každý záznam říká, co se objeví, v které sekundě, v jaké výšce a jak se pohybuje. Pohyb je seznam bodů, kterými nepřítel letí stejnou rychlostí. Volitelně přibudou časy výstřelů, výpad bosse nebo generátor menších nepřátel.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">každý snímek:
    čas = teď - začátek levelu - čas v pauze
    pro každý záznam v tabulce levelu:
        pokud záznam ještě nebyl použit a čas &gt; záznam.čas:
            vytvoř objekt podle záznamu
            označ záznam jako použitý</code></pre>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/level1.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/level1.png" alt="Časová osa prvního levelu: zleva doprava sekundy, výška ikon odpovídá výšce, ve které se nepřítel objeví" width="1004" height="120" loading="lazy" decoding="async" /></picture><figcaption>První level: 39 záznamů za 67 sekund. Komety, pak vlny raket, power-upy (červené) a na konci boss.</figcaption></figure>
<p>Takto zapsaný level se snadno ladí: posunout vlnu o půl sekundy znamená změnit jedno číslo. Na časové ose je vidět i rytmus, o který jde: krátká vlna, pauza, jiný typ nepřítele, power-up před těžším úsekem.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/path.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/path.png" alt="Raketa letící cikcak přes herní plochu, body zlomu dráhy jsou vyznačené" width="1008" height="576" loading="lazy" decoding="async" /></picture><figcaption>Dráha rakety z prvního levelu: šest bodů, kterými letí stejnou rychlostí.</figcaption></figure>
<p>Nevýhodu cítit při větších změnách. Nejhustší level naší hry má 71 řádků a taková tabulka se bez editoru čte špatně. Výhodnější je zapisovat levely jako vlny (skupina nepřátel s jedním vzorem pohybu), ne jako jednotlivé lodě.</p>
<h2 id="nekonecny-rezim-z-jednoho-pseudonahodneho-cisla"><a class="heading-anchor" href="#nekonecny-rezim-z-jednoho-pseudonahodneho-cisla" aria-hidden="true" tabindex="-1">#</a>Nekonečný režim z jednoho pseudonáhodného čísla</h2>
<p>Pro žebříček (v naší hře Game Center) potřebujete režim, který nekončí. Ručně psanou tabulku v něm nahradí generátor, který ji vyrábí za běhu. Vyrobí 20ciferné pseudonáhodné číslo a jednotlivé dvojice číslic rozhodnou o jedné vlně:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">číslo = další pseudonáhodné 20ciferné číslo
typ = power-up, pokud číslice 4 až 5 &gt; 90, jinak nepřítel
sprite = číslice 1 až 2 modulo počet druhů nepřátel
pauza před vlnou = číslice 3 / 2 sekundy (aspoň 1)
výška = číslice 7, přepočtená na výšku plochy
vzor pohybu = číslice 10 až 11 modulo 10
počet lodí ve vlně = číslice 17 / 2 + 1
rychlost = číslice 13 × (1 + pořadí vlny / 10), v rozsahu 20 až 50
každá třicátá vlna = boss, generování stojí, dokud boss žije</code></pre>
<p>Seed generátoru (počáteční hodnotu) nechte ve hře pevný. Každý hráč pak dostane stejné pořadí vln a skóre v žebříčku se dá porovnávat.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/patterns.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/patterns.png" alt="Deset malých herních ploch, na každé jeden vzor pohybu nepřítele vyznačený přerušovanou čarou" width="912" height="228" loading="lazy" decoding="async" /></picture><figcaption>Deset vzorů pohybu z generátoru. Konkrétní tvar závisí na výšce, ve které se vlna objeví.</figcaption></figure>
<p>Jak se takový režim vyvíjí, ukazuje simulace generátoru mimo hru (300 vln, stejný seed jako ve hře). První boss přijde po 29 vlnách, tedy po 74 nepřátelích a 3 power-upech, zhruba po 130 sekundách. Prvních 30 vln letí minimální rychlostí 20, protože jedna číslice je ještě menší než dolní hranice. Průměrná rychlost pak roste na 28,5, 37,5 a 41, od 120. vlny je většina nepřátel na maximu. Power-up je přibližně každá jedenáctá vlna.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/shmup-apple-watch-devlog/ranked.webp" /><img src="https://smallwater.eu/blog/shmup-apple-watch-devlog/ranked.png" alt="Časová osa prvních 135 sekund nekonečného režimu s ikonami nepřátel a bossem na konci" width="1104" height="120" loading="lazy" decoding="async" /></picture><figcaption>Prvních 135 sekund nekonečného režimu podle simulace generátoru. Vlny jsou delší a nepravidelnější než v ručně psaném levelu.</figcaption></figure>
<p>Slabina je, že generátor nezná rytmus. Ruční level střídá tlak a oddech, generátor ne. Částečně to řeší pauza během bosse a růst rychlosti, ale ručně psané levely se hrají lépe.</p>
<h2 id="limity-tohoto-pristupu"><a class="heading-anchor" href="#limity-tohoto-pristupu" aria-hidden="true" tabindex="-1">#</a>Limity tohoto přístupu</h2>
<ul>
<li><strong>Žádné automatické testy.</strong> Všechno se ověřuje ručně v simulátoru a na hodinkách.</li>
<li><strong>Globální stav.</strong> Většina stavu hry jsou globální proměnné jedné scény. Pro hru jen na hodinky je to pohodlné, při přidání verze pro iPhone už ne.</li>
<li><strong>Levely bez editoru.</strong> Tabulka časů se dobře ladí po malých krocích, při větších změnách by si zasloužila editor.</li>
</ul>
<p>Pokud děláte hru na hodinky nebo jinou malou obrazovku a potřebujete s něčím pomoct, <a href="https://smallwater.eu/cs/#kontakt">napište nám</a>. Jak generujeme hlavolamy v jiné naší aplikaci, jsme popsali v článku o <a href="https://smallwater.eu/cs/blog/sudoku-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/shmup-apple-watch-devlog/" data-ga-destination="/cs/blog/sudoku-generator/" data-ga-placement="inline">generátoru sudoku</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak vygenerovat sudoku s jediným řešením</title>
      <link>https://smallwater.eu/cs/blog/sudoku-generator/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/sudoku-generator/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Křížovky</category>
      <description>Návod na generátor sudoku: rychlý solver s bitovými maskami, náhodná plná mřížka, mazání s kontrolou jediného řešení a nastavení obtížnosti.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/sudoku-generator/cover.webp" /><img src="https://smallwater.eu/blog/sudoku-generator/cover.jpg" alt="Rozpracované sudoku s malými číslicemi kandidátů a zvýrazněným políčkem s jediným kandidátem" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#proc-prave-jedno-reseni" data-ga="blog_toc_clicked" data-ga-heading-id="proc-prave-jedno-reseni">Proč právě jedno řešení</a></li><li><a href="#krok-1-napiste-rychly-solver" data-ga="blog_toc_clicked" data-ga-heading-id="krok-1-napiste-rychly-solver">Krok 1: Napište rychlý solver</a></li><li><a href="#krok-2-vyplnte-celou-mrizku" data-ga="blog_toc_clicked" data-ga-heading-id="krok-2-vyplnte-celou-mrizku">Krok 2: Vyplňte celou mřížku</a></li><li><a href="#proc-nestaci-smazat-nahodna-policka" data-ga="blog_toc_clicked" data-ga-heading-id="proc-nestaci-smazat-nahodna-policka">Proč nestačí smazat náhodná políčka</a></li><li><a href="#krok-3-mazte-s-kontrolou-jednoznacnosti" data-ga="blog_toc_clicked" data-ga-heading-id="krok-3-mazte-s-kontrolou-jednoznacnosti">Krok 3: Mažte s kontrolou jednoznačnosti</a></li><li><a href="#krok-4-nastavte-obtiznost-poctem-prazdnych-policek" data-ga="blog_toc_clicked" data-ga-heading-id="krok-4-nastavte-obtiznost-poctem-prazdnych-policek">Krok 4: Nastavte obtížnost počtem prázdných políček</a></li><li><a href="#rychlost-a-spolehlivost-generatoru" data-ga="blog_toc_clicked" data-ga-heading-id="rychlost-a-spolehlivost-generatoru">Rychlost a spolehlivost generátoru</a></li><li><a href="#kde-pocet-prazdnych-policek-nestaci" data-ga="blog_toc_clicked" data-ga-heading-id="kde-pocet-prazdnych-policek-nestaci">Kde počet prázdných políček nestačí</a></li></ol></nav><p>Sudoku s jediným řešením vygenerujete ve dvou krocích. Nejdřív vyplňte prázdnou mřížku 9×9 celou, backtrackingem s náhodným pořadím číslic. Potom z ní po jednom a v náhodném pořadí mažte políčka a po každém smazání solverem zkontrolujte, jestli má zadání stále právě jedno řešení. Pokud ne, číslici vraťte zpět. Na běžném procesoru to trvá v mediánu méně než milisekundu.</p>
<p>Postup se hodí pro každou hru nebo aplikaci, která potřebuje neomezený počet nových zadání bez ruční přípravy. Takto vznikají sudoku například v naší aplikaci s křížovkami a hlavolamy (<a href="https://smallwater.eu/crosswords-ios">App Store</a>, <a href="https://smallwater.eu/crosswords-android">Google Play</a>).</p>
<h2 id="proc-prave-jedno-reseni"><a class="heading-anchor" href="#proc-prave-jedno-reseni" aria-hidden="true" tabindex="-1">#</a>Proč právě jedno řešení</h2>
<p>Pravidla sudoku zná každý: v každém řádku, sloupci a čtverci 3×3 je každá číslice 1 až 9 právě jednou. Méně se mluví o tom, že dobré zadání má <strong>jediné</strong> řešení. Teprve pak se dá vyřešit čistou logikou, bez hádání.</p>
<p>Má to i praktický důsledek. Hra si obvykle pamatuje jedno řešení a podle něj kontroluje výsledek a dává nápovědy. Pokud má zadání řešení dvě, hráč může dojít k tomu druhému, které je stejně správné, a hra mu ho neuzná.</p>
<h2 id="krok-1-napiste-rychly-solver"><a class="heading-anchor" href="#krok-1-napiste-rychly-solver" aria-hidden="true" tabindex="-1">#</a>Krok 1: Napište rychlý solver</h2>
<p>Solver je program, který sudoku vyřeší. Pracuje backtrackingem: vyplní políčko jednou z možných číslic, pokračuje dalším políčkem, a když narazí na spor, vrátí se a zkusí jinou číslici. Budete ho potřebovat dvakrát: na vyplnění prázdné mřížky a na kontrolu jednoznačnosti. Kontrola běží po každém smazaném políčku, tedy 40 až 70krát na jedno zadání, proto musí být rychlá. Rozhodují dvě věci:</p>
<ul>
<li><strong>Bitové masky.</strong> Pro každý řádek, sloupec a čtverec udržujte 9bitové číslo s už použitými číslicemi. Kandidáti políčka, tedy číslice, které do něj ještě můžou přijít, jsou pak jedna operace: všechny číslice kromě těch, které jsou v jeho řádku, sloupci nebo čtverci. Počet kandidátů je počet jedničkových bitů a dá se předem spočítat do tabulky s 512 hodnotami.</li>
<li><strong>Nejdřív políčko s nejmenším počtem kandidátů.</strong> Nevybírejte první prázdné políčko zleva, ale to, které má nejméně možností (v literatuře heuristika MRV, minimum remaining values). Políčko s jediným kandidátem vyplňte hned, políčko bez kandidáta znamená slepou větev a vracíte se.</li>
</ul>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/sudoku-generator/candidates.webp" /><img src="https://smallwater.eu/blog/sudoku-generator/candidates.png" alt="Rozpracovaná mřížka s malými číslicemi kandidátů v prázdných políčkách, jedno políčko s jediným kandidátem je zvýrazněné" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Kandidáti v prázdných políčkách. Zvýrazněné políčko má jediného kandidáta, solver ho vyplní jako první.</figcaption></figure>
<p>Na těžkých zadáních potřebuje kontrola jednoznačnosti s tímto pravidlem v mediánu 311 kroků. Pokud solver bere políčka postupně zleva doprava, v mediánu 28 579 kroků a v nejhorších 5 % případů přes 300 000. Rozdíl je zhruba 90násobný.</p>
<h2 id="krok-2-vyplnte-celou-mrizku"><a class="heading-anchor" href="#krok-2-vyplnte-celou-mrizku" aria-hidden="true" tabindex="-1">#</a>Krok 2: Vyplňte celou mřížku</h2>
<p>Plnou mřížku dostanete stejným solverem spuštěným na prázdné mřížce. Jediný rozdíl je, že kandidáty políčka zkoušíte v náhodném pořadí. První nalezené řešení je náhodná, platná, úplně vyplněná mřížka.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/sudoku-generator/solved.webp" /><img src="https://smallwater.eu/blog/sudoku-generator/solved.png" alt="Úplně vyplněná mřížka sudoku 9×9" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Náhodná plná mřížka. Z ní vznikne zadání.</figcaption></figure>
<h2 id="proc-nestaci-smazat-nahodna-policka"><a class="heading-anchor" href="#proc-nestaci-smazat-nahodna-policka" aria-hidden="true" tabindex="-1">#</a>Proč nestačí smazat náhodná políčka</h2>
<p>Nejjednodušší nápad je smazat z plné mřížky N náhodných políček, například 40, 50 nebo 55 podle obtížnosti. Zadání vypadá v pořádku, jen často nemá jediné řešení.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/sudoku-generator/ambiguous.webp" /><img src="https://smallwater.eu/blog/sudoku-generator/ambiguous.png" alt="Zadání sudoku, ve kterém čtyři políčka v rozích obdélníku mají dvě možné dvojice číslic" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Zadání z náhodného mazání. Ve zvýrazněných políčkách sedí obě dvojice číslic, zadání má dvě řešení.</figcaption></figure>
<p>Typický případ je na obrázku. Čtyři políčka v rozích obdélníku, který zasahuje do dvou čtverců, se dají vyplnit dvěma způsoby a zbytek mřížky to nijak nerozhodne. Řešitelé sudoku tento vzor znají jako deadly pattern. Na 1 000 zadáních pro každou obtížnost vypadá výsledek takto:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Obtížnost</th>
<th align="right">Smazaných políček</th>
<th align="right">Zadání s více než jedním řešením</th>
</tr>
</thead>
<tbody><tr>
<td>Lehká</td>
<td align="right">40</td>
<td align="right">59 %</td>
</tr>
<tr>
<td>Střední</td>
<td align="right">50</td>
<td align="right">96 %</td>
</tr>
<tr>
<td>Těžká</td>
<td align="right">55</td>
<td align="right">100 %</td>
</tr>
</tbody></table>
</div>
<p>Těžké zadání z náhodného mazání má v průměru stovky řešení. Kontrola jednoznačnosti po každém smazání je proto nutná.</p>
<h2 id="krok-3-mazte-s-kontrolou-jednoznacnosti"><a class="heading-anchor" href="#krok-3-mazte-s-kontrolou-jednoznacnosti" aria-hidden="true" tabindex="-1">#</a>Krok 3: Mažte s kontrolou jednoznačnosti</h2>
<p>Projděte všech 81 políček v náhodném pořadí. Každé zkuste smazat a solverem spočítejte řešení, ale počítání zastavte u dvou. Víc vědět nepotřebujete: jedno řešení znamená, že políčko může zůstat prázdné, dvě znamenají, že ho musíte vrátit.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">zadání = kopie plné mřížky
prázdných = 0
pro každé políčko v náhodném pořadí:
    pokud prázdných == cíl: konec
    číslice = zadání[políčko]
    zadání[políčko] = prázdné
    pokud počet_řešení(zadání, nejvýš 2) == 1:
        prázdných = prázdných + 1
    jinak:
        zadání[políčko] = číslice</code></pre>
<p>Políčko, které jednou vrátíte, už nezkoušejte znovu. Další mazání zadání jen uvolňuje, takže pokud políčko nešlo smazat teď, nepůjde ani později.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/sudoku-generator/dig.webp" /><img src="https://smallwater.eu/blog/sudoku-generator/dig.png" alt="Rozpracované těžké zadání s 37 prázdnými políčky a jednou zvýrazněnou číslicí, kterou bylo nutné vrátit" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Po 37 smazaných políčkách. Zvýrazněnou číslici se smazat nepodařilo, zadání by mělo dvě řešení, proto se vrátila.</figcaption></figure>
<h2 id="krok-4-nastavte-obtiznost-poctem-prazdnych-policek"><a class="heading-anchor" href="#krok-4-nastavte-obtiznost-poctem-prazdnych-policek" aria-hidden="true" tabindex="-1">#</a>Krok 4: Nastavte obtížnost počtem prázdných políček</h2>
<p>Nejjednodušší míra obtížnosti je počet smazaných políček. Tři úrovně, které používáme v naší hře:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Obtížnost</th>
<th align="right">Prázdných políček</th>
<th align="right">Zbývá číslic</th>
<th align="right">Průměrně vrácených políček</th>
</tr>
</thead>
<tbody><tr>
<td>Lehká</td>
<td align="right">40</td>
<td align="right">41</td>
<td align="right">0,9</td>
</tr>
<tr>
<td>Střední</td>
<td align="right">50</td>
<td align="right">31</td>
<td align="right">4,5</td>
</tr>
<tr>
<td>Těžká</td>
<td align="right">55</td>
<td align="right">26</td>
<td align="right">12,7</td>
</tr>
</tbody></table>
</div>
<p>Čím méně číslic zbývá, tím častěji by další smazání pokazilo jednoznačnost a políčko se musí vrátit. U těžké úrovně se jeden průchod občas zastaví dřív, než dosáhne 55 prázdných políček. Pak vygenerujte novou plnou mřížku a zkuste to znovu. Při měření na 1 000 těžkých zadáních stačil první průchod v 97,6 % případů a nikdy nebylo potřeba víc než 3 pokusy.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/sudoku-generator/final.webp" /><img src="https://smallwater.eu/blog/sudoku-generator/final.png" alt="Hotové těžké zadání sudoku s 26 číslicemi" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Hotové těžké zadání: 26 číslic a jediné řešení.</figcaption></figure>
<h2 id="rychlost-a-spolehlivost-generatoru"><a class="heading-anchor" href="#rychlost-a-spolehlivost-generatoru" aria-hidden="true" tabindex="-1">#</a>Rychlost a spolehlivost generátoru</h2>
<p>Generátor jsme spustili na 1 000 zadáních pro každou obtížnost (Node.js 22 na serverovém procesoru, telefon bude pomalejší):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Obtížnost</th>
<th align="right">Medián času</th>
<th align="right">95 % zadání do</th>
<th align="right">Nejdéle</th>
<th align="right">Zadání s více řešeními</th>
</tr>
</thead>
<tbody><tr>
<td>Lehká</td>
<td align="right">0,13 ms</td>
<td align="right">0,29 ms</td>
<td align="right">3,5 ms</td>
<td align="right">0</td>
</tr>
<tr>
<td>Střední</td>
<td align="right">0,21 ms</td>
<td align="right">0,38 ms</td>
<td align="right">0,8 ms</td>
<td align="right">0</td>
</tr>
<tr>
<td>Těžká</td>
<td align="right">0,48 ms</td>
<td align="right">1,39 ms</td>
<td align="right">5,5 ms</td>
<td align="right">0</td>
</tr>
</tbody></table>
</div>
<p>Každé vygenerované zadání jsme navíc nezávisle zkontrolovali solverem. Všech 3 000 mělo jediné řešení.</p>
<h2 id="kde-pocet-prazdnych-policek-nestaci"><a class="heading-anchor" href="#kde-pocet-prazdnych-policek-nestaci" aria-hidden="true" tabindex="-1">#</a>Kde počet prázdných políček nestačí</h2>
<p>Počet vodítek je hrubá míra obtížnosti. Dvě zadání s 26 číslicemi můžou být různě těžká podle toho, jaké logické techniky vyžadují. Přesnější obtížnost by zadání vyřešila jako člověk a zaznamenala nejtěžší techniku, kterou musela použít. Pro mobilní hru, kde si hráč vybírá ze tří úrovní, počet vodítek stačí.</p>
<aside class="app-box app-box-callout"><img class="app-box-img" src="https://smallwater.eu/images/crosswords/1-crosswords.jpg" alt="" width="698" height="1516" loading="lazy" decoding="async" /><div class="app-box-body"><p class="app-box-eyebrow">Vyzkoušejte</p><h2 class="app-box-title">Křížovky</h2><p>Křížovky, osmisměrky, sudoku a malované křížovky v jedné offline aplikaci. Jak hádanky vznikají a co je nového.</p><div class="app-box-links"><a class="btn btn-primary" href="https://smallwater.eu/crosswords-ios" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="crosswords" data-ga-destination="/crosswords-ios" data-ga-app="crosswords" data-ga-store="app_store">App Store</a><a class="btn btn-ghost" href="https://smallwater.eu/crosswords-android" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="crosswords" data-ga-destination="/crosswords-android" data-ga-app="crosswords" data-ga-store="google_play">Google Play</a></div></div></aside>
<p>Pokud řešíte podobný problém, generujete hlavolamy nebo potřebujete mobilní aplikaci, <a href="https://smallwater.eu/cs/#kontakt">napište nám</a>. Jak vygenerovat osmisměrku s tajenkou, popisuje <a href="https://smallwater.eu/cs/blog/word-search-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/sudoku-generator/" data-ga-destination="/cs/blog/word-search-generator/" data-ga-placement="inline">samostatný návod</a>. Švédské křížovce s tajenkou se věnuje <a href="https://smallwater.eu/cs/blog/swedish-crossword-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/sudoku-generator/" data-ga-destination="/cs/blog/swedish-crossword-generator/" data-ga-placement="inline">další návod</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak vygenerovat švédskou křížovku s tajenkou</title>
      <link>https://smallwater.eu/cs/blog/swedish-crossword-generator/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/swedish-crossword-generator/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Křížovky</category>
      <description>Návod na generátor švédské křížovky: maska přes hill climbing, plnění slovy jako CSP s backtrackingem a tajenka hledaná až v hotové mřížce.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/swedish-crossword-generator/cover.webp" /><img src="https://smallwater.eu/blog/swedish-crossword-generator/cover.jpg" alt="Dvě švédské křížovky 10×10: vlevo náhodná maska s penalizovanými políčky, vpravo vyplněná mřížka s korálovými písmeny tajenky" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#co-musi-svedska-krizovka-splnovat" data-ga="blog_toc_clicked" data-ga-heading-id="co-musi-svedska-krizovka-splnovat">Co musí švédská křížovka splňovat</a></li><li><a href="#proc-nestaci-plnit-mrizku-slovo-po-slove" data-ga="blog_toc_clicked" data-ga-heading-id="proc-nestaci-plnit-mrizku-slovo-po-slove">Proč nestačí plnit mřížku slovo po slově</a></li><li><a href="#krok-1-maska-pres-hill-climbing" data-ga="blog_toc_clicked" data-ga-heading-id="krok-1-maska-pres-hill-climbing">Krok 1: maska přes hill climbing</a></li><li><a href="#krok-2-plneni-slovy-jako-csp" data-ga="blog_toc_clicked" data-ga-heading-id="krok-2-plneni-slovy-jako-csp">Krok 2: plnění slovy jako CSP</a></li><li><a href="#krok-3-tajenka-az-v-hotove-mrizce" data-ga="blog_toc_clicked" data-ga-heading-id="krok-3-tajenka-az-v-hotove-mrizce">Krok 3: tajenka až v hotové mřížce</a></li><li><a href="#proc-nevkladat-tajenku-predem-jako-v-novinach" data-ga="blog_toc_clicked" data-ga-heading-id="proc-nevkladat-tajenku-predem-jako-v-novinach">Proč nevkládat tajenku předem jako v novinách</a></li><li><a href="#vykon-generatoru-a-proc-nebezi-v-telefonu" data-ga="blog_toc_clicked" data-ga-heading-id="vykon-generatoru-a-proc-nebezi-v-telefonu">Výkon generátoru a proč neběží v telefonu</a></li><li><a href="#generovani-ve-velkem" data-ga="blog_toc_clicked" data-ga-heading-id="generovani-ve-velkem">Generování ve velkém</a></li><li><a href="#co-reseni-nepokryva" data-ga="blog_toc_clicked" data-ga-heading-id="co-reseni-nepokryva">Co řešení nepokrývá</a></li></ol></nav><p>Švédskou křížovku s tajenkou vygenerujete ve dvou fázích: nejdřív lokálním prohledáváním najděte masku, tedy rozmístění políček s legendami a políček na písmena, a potom ji vyplňte slovy ze slovníku jako úlohu s omezeními (CSP) s backtrackingem. Tajenku do mřížky předem nevkládejte. Teprve když je křížovka hotová, hledejte vtip, jehož pointa se v ní dá přečíst zleva doprava a shora dolů.</p>
<p>Takto vznikají švédské křížovky v naší aplikaci s křížovkami a osmisměrkami (<a href="https://smallwater.eu/crosswords-ios">App Store</a>, <a href="https://smallwater.eu/crosswords-android">Google Play</a>). Jedna mřížka zabere několik až desítky sekund procesorového času, proto je generujeme předem na výkonnějším počítači a přibalíme je do aplikace. Osmisměrka se generuje úplně jinak a jednodušeji, postup je v článku <a href="https://smallwater.eu/cs/blog/word-search-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/swedish-crossword-generator/" data-ga-destination="/cs/blog/word-search-generator/" data-ga-placement="inline">Jak vygenerovat osmisměrku s tajenkou</a>.</p>
<h2 id="co-musi-svedska-krizovka-splnovat"><a class="heading-anchor" href="#co-musi-svedska-krizovka-splnovat" aria-hidden="true" tabindex="-1">#</a>Co musí švédská křížovka splňovat</h2>
<p>Švédská křížovka nemá černá políčka. Každé políčko je písmeno, nebo legenda se šipkou doprava či dolů. Z toho plynou tvrdé podmínky:</p>
<ul>
<li>Každé písmeno patří vodorovnému i svislému slovu. Písmeno, které kříží jen jedno slovo, nejde zkontrolovat a v křížovce působí jako chyba.</li>
<li>Každé slovo má aspoň dvě písmena a před ním je legenda. Proto jsou první řádek a první sloupec vždy tvořené jen legendami.</li>
<li>Písmena tvoří jeden souvislý celek, žádné odtržené ostrovy.</li>
<li>Každé slovo musí být ve slovníku a mít aspoň jednu legendu. Žádné slovo se v mřížce neopakuje.</li>
</ul>
<p>K tomu měkká kritéria kvality: málo dvoupísmenných slov, žádné legendy, které nikam neukazují, a co nejméně velmi dlouhých slov, která se těžko plní. Problém je výpočetně těžký. <a href="https://link.springer.com/chapter/10.1007/978-3-642-30347-0_15" target="_blank" rel="noopener">Engel a kol. (2012)</a> ukázali, že už samotné plnění masky slovy je NP-úplné.</p>
<h2 id="proc-nestaci-plnit-mrizku-slovo-po-slove"><a class="heading-anchor" href="#proc-nestaci-plnit-mrizku-slovo-po-slove" aria-hidden="true" tabindex="-1">#</a>Proč nestačí plnit mřížku slovo po slově</h2>
<p>První nápad je postupovat jako člověk s tužkou: rozdělit tajenku na slova, položit je do řádku jako v novinách, přes každé její písmeno vést křížící slovo a zbytek mřížky doplňovat greedy algoritmem. Greedy algorithm v každém kroku vybere možnost, která vypadá nejlépe právě teď, a rozhodnutí už nikdy nepřehodnotí. Kde nic nesedí, zůstane díra. A protože se greedy plnění neumí vrátit, špatných prvních deset slov zbytek mřížky už nezachrání.</p>
<p>Lepší heuristika výběru slov pomůže jen trochu. Můžete zkoušet délky křížících slov od středních, kolem pěti písmen, ke krajním. Nebo použít evoluční přístup: populace rozpracovaných křížovek, z každé pět potomků s jedním slovem navíc, hodnocení podle počtu zaplněných políček, počtu samohlásek a rovnoměrnosti písmen a do další generace postoupí jen ty nejlepší. Výsledky jsou o něco hustší, ale díry zůstanou, protože heuristika vidí jen nejbližší krok.</p>
<p>Řešení přináší bakalářská práce Jakoba Engela z TU Mnichov (2009), který je i prvním autorem zmíněného článku. Úlohu rozdělte na dvě nezávislé fáze: nejdřív najděte dobrou masku bez ohledu na slova, potom ji plňte. Engel na masku použil evoluční algoritmus. My používáme jednodušší lokální prohledávání s náhodnými otřesy, popsané v dalším kroku.</p>
<h2 id="krok-1-maska-pres-hill-climbing"><a class="heading-anchor" href="#krok-1-maska-pres-hill-climbing" aria-hidden="true" tabindex="-1">#</a>Krok 1: maska přes hill climbing</h2>
<p>Maska je mřížka, ve které je každé políčko buď legenda, nebo písmeno. K jejímu hledání stačí hill climbing: lokální prohledávání, které dělá malé změny a ponechá ty, které řešení nezhorší.</p>
<p>Začněte náhodně, s legendami asi na 22 % vnitřních políček. Každou masku ohodnoťte pokutou. Tvrdá porušení (příliš krátké slovo, písmeno bez křížícího slova, odtržený ostrov) ať stojí většinou 1 000 bodů, měkké nedostatky (dvoupísmenná slova nad rozpočet, legenda, která neukazuje na žádné slovo, shluky legend) 50 až 500.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/swedish-crossword-generator/mask-random.webp" /><img src="https://smallwater.eu/blog/swedish-crossword-generator/mask-random.png" alt="Náhodná maska 10×10 s množstvím červeně a oranžově zvýrazněných problémových políček" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Náhodná maska. Červená políčka jsou písmena s porušením, oranžová legendy, které nikam neukazují. Pokuta 35 500.</figcaption></figure>
<p>Potom v cyklu změňte jedno políčko z legendy na písmeno nebo naopak, případně posuňte legendu o políčko šikmo. Změnu ponechte, pokud se pokuta nezvýšila, jinak ji vraťte. Polovinu změn zamiřte na políčka s nejvyšší pokutou, pak hrubé chyby zmizí rychle:</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/swedish-crossword-generator/mask-mid.webp" /><img src="https://smallwater.eu/blog/swedish-crossword-generator/mask-mid.png" alt="Stejná maska po 150 krocích, problémových políček zbylo jen několik" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Po 150 krocích. Pokuta 1 770.</figcaption></figure>
<p>Hill climbing snadno uvízne v lokálním minimu. Když se za 2 000 kroků nic nezlepší, mřížkou „zatřeste“: v nejlepší dosud nalezené masce náhodně přehoďte pět políček a hledejte dál. V grafu jsou to špičky. Pokud ani po třech zatřeseních není maska lepší a nemá tvrdá porušení, skončete.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/swedish-crossword-generator/penalty.webp" /><img src="https://smallwater.eu/blog/swedish-crossword-generator/penalty.png" alt="Graf pokuty v logaritmickém měřítku během 8 300 kroků: rychlý pád z 35 500 na 200 a tři krátké špičky po zatřesení" width="1800" height="850" loading="lazy" decoding="async" /></picture><figcaption>Pokuta během generování masky 10×10. Body označují dva předchozí snímky.</figcaption></figure>
<p>Výsledná maska má zbytkovou pokutu 200 za měkké nedostatky, ale žádné tvrdé porušení:</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/swedish-crossword-generator/mask-final.webp" /><img src="https://smallwater.eu/blog/swedish-crossword-generator/mask-final.png" alt="Hotová maska 10×10 se šipkami v políčkách legend a prázdnými políčky na písmena" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Hotová maska. Šipky ukazují směr slova.</figcaption></figure>
<p>Pokuty za legendu bez slova a za odtržený ostrov se vyplatí mít v masce od začátku. Na velkém souboru hotových křížovek bez nich mělo 13,2 % legend, které neukazovaly na žádné slovo, a 23,5 % mřížek odtržený ostrov písmen. S nimi je to 5,0 % a 0 %.</p>
<h2 id="krok-2-plneni-slovy-jako-csp"><a class="heading-anchor" href="#krok-2-plneni-slovy-jako-csp" aria-hidden="true" tabindex="-1">#</a>Krok 2: plnění slovy jako CSP</h2>
<p>Z masky vznikne seznam slotů, tedy míst na slova se známou délkou a kříženími. Plnění je klasická úloha s omezeními (constraint satisfaction problem, CSP): každý slot je proměnná, jeho doména jsou slova, která se do něj vejdou, a omezení jsou shodná písmena v kříženích. Řeší se backtrackingem, tedy prohledáváním, které se ve slepé větvi vrátí o krok zpět a zkusí jinou možnost:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">pro každý slot: doména = všechna slova dané délky
dokud nejsou všechny sloty vyplněné:
    vyber slot s nejmenší doménou (při shodě ten s nejvíc kříženími)
    pro každé slovo z domény, od nejlepšího:
        polož slovo
        zuž domény křížících slotů na slova se shodným písmenem
        pokud některá doména zůstala prázdná: vrať změnu, zkus další slovo
        jinak pokračuj dalším slotem
    pokud žádné slovo nevyhovuje: vrať se o slot zpět
po 50 000 krocích to vzdej</code></pre>
<p>V literatuře se tyto heuristiky jmenují MRV (minimum remaining values, nejdřív slot s nejméně možnostmi) a forward checking (po každém položení zúžit domény sousedů, aby se slepá větev odhalila hned, ne o deset slov později). Slovník si předpočítejte podle délky, pozice a písmene. Slova se „Š na třetím místě“ jsou pak jeden průnik množin, ne průchod celým slovníkem.</p>
<p>Důležité je i pořadí slov. Přednost dejte slovům, která mají ve slovníku víc různých legend. Je to jednoduchý ukazatel běžného křížovkářského slova. Bez něj se mřížka zaplní neznámými zkratkami.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/swedish-crossword-generator/filled.webp" /><img src="https://smallwater.eu/blog/swedish-crossword-generator/filled.png" alt="Maska 10×10 vyplněná anglickými slovy" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Vyplněná mřížka. Obrázky v článku používají anglický slovník, čísla v tabulkách jsou ze slovenského.</figcaption></figure>
<h2 id="krok-3-tajenka-az-v-hotove-mrizce"><a class="heading-anchor" href="#krok-3-tajenka-az-v-hotove-mrizce" aria-hidden="true" tabindex="-1">#</a>Krok 3: tajenka až v hotové mřížce</h2>
<p>Při plnění ještě nevíte, jaká bude tajenka. Přečtěte písmena hotové mřížky zleva doprava a shora dolů a hledejte vtip, jehož pointa je v tomto řetězci podposloupností. Písmena tedy jdou ve správném pořadí, ale nemusí být vedle sebe. Pokud se najde, rozmístěte písmena tajenky co nejrovnoměrněji po mřížce, aby se pointa nedala uhodnout z jednoho slova.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/swedish-crossword-generator/secret-order.webp" /><img src="https://smallwater.eu/blog/swedish-crossword-generator/secret-order.png" alt="Vyplněná mřížka s deseti korálovými písmeny S, P, A, C, E, S, U, I, T, S očíslovanými v pořadí čtení" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Tajenka SPACESUITS v pořadí čtení. Do této mřížky by se vešlo 111 různých point z našeho anglického seznamu vtipů.</figcaption></figure>
<p>Toto pořadí (nejdřív mřížka, potom vtip) zásadně zvyšuje počet hotových křížovek. Jedna mřížka 12×12 se podle našeho měření v průměru hodí pro 86 vtipů ze slovenského seznamu (5 960 vtipů). Vždy se tedy najde takový, který ještě v dané velikosti nebyl použit.</p>
<h2 id="proc-nevkladat-tajenku-predem-jako-v-novinach"><a class="heading-anchor" href="#proc-nevkladat-tajenku-predem-jako-v-novinach" aria-hidden="true" tabindex="-1">#</a>Proč nevkládat tajenku předem jako v novinách</h2>
<p>V novinách je tajenka obvykle pohromadě, v jednom řádku a rozdělená na slova. Dá se toho dosáhnout dvěma způsoby a oba dávají velmi málo výsledků.</p>
<p>První způsob je vložit tajenku do mřížky předem. Pak se do ní musí vejít. Pointa má u nás v průměru 10,3 písmene a 2 slova. Takto vypadá podíl vtipů ze slovenského seznamu, které se do dané velikosti vůbec dají položit:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Mřížka</th>
<th align="right">Celá pointa v jednom řádku</th>
<th align="right">Pointa rozdělená na slova (nejvýš 3)</th>
</tr>
</thead>
<tbody><tr>
<td>8×8</td>
<td align="right">0 %</td>
<td align="right">22 %</td>
</tr>
<tr>
<td>10×10</td>
<td align="right">0,1 %</td>
<td align="right">64 %</td>
</tr>
<tr>
<td>12×12</td>
<td align="right">45 %</td>
<td align="right">89 %</td>
</tr>
<tr>
<td>15×15</td>
<td align="right">84 %</td>
<td align="right">98 %</td>
</tr>
</tbody></table>
</div>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/swedish-crossword-generator/newspaper.webp" /><img src="https://smallwater.eu/blog/swedish-crossword-generator/newspaper.png" alt="Mřížka 10×10, ve které jsou slova tajenky KNOW a RULES položena předem jako celá vodorovná slova v korálové barvě" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Tajenka vložená předem jako celá slova v řádcích, tak jako v novinách. Vygenerováno stejným generátorem.</figcaption></figure>
<p>S tajenkou vloženou předem uspěje plnění u malých mřížek přibližně stejně často jako bez ní. U 15×15 to bylo 5 z 60 pokusů oproti 8 bez tajenky, u 18×18 ani jeden z 60. Každá taková mřížka navíc slouží jedinému vtipu a u malých velikostí se většina vtipů nedá ani položit.</p>
<p>Druhý způsob je nechat mřížku vzniknout volně a pointu v ní hledat jako souvislý úsek. Na vzorku 2 200 hotových slovenských křížovek (200 z každé velikosti od 8×8 po 18×18) se celá pointa jako souvislý úsek v jednom řádku nebo sloupci našla jen v 81 mřížkách (3,7 %). Když smí každé slovo pointy ležet jinde, je to 443 mřížek (20 %), a to ještě bez podmínky, že slova budou u sebe. Při čtení v pořadí se vtip našel v každé z nich. Za cenu tajenky roztroušené po mřížce dostanete řádově víc křížovek.</p>
<aside class="app-box app-box-callout"><img class="app-box-img" src="https://smallwater.eu/images/crosswords/1-crosswords.jpg" alt="" width="698" height="1516" loading="lazy" decoding="async" /><div class="app-box-body"><p class="app-box-eyebrow">Vyzkoušejte</p><h2 class="app-box-title">Křížovky</h2><p>Křížovky, osmisměrky, sudoku a malované křížovky v jedné offline aplikaci. Jak hádanky vznikají a co je nového.</p><div class="app-box-links"><a class="btn btn-primary" href="https://smallwater.eu/crosswords-ios" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="crosswords" data-ga-destination="/crosswords-ios" data-ga-app="crosswords" data-ga-store="app_store">App Store</a><a class="btn btn-ghost" href="https://smallwater.eu/crosswords-android" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="crosswords" data-ga-destination="/crosswords-android" data-ga-app="crosswords" data-ga-store="google_play">Google Play</a></div></div></aside>
<h2 id="vykon-generatoru-a-proc-nebezi-v-telefonu"><a class="heading-anchor" href="#vykon-generatoru-a-proc-nebezi-v-telefonu" aria-hidden="true" tabindex="-1">#</a>Výkon generátoru a proč neběží v telefonu</h2>
<p>Generátor běžel 60 pokusů pro každou velikost se slovenským slovníkem, v jednom vlákně (JavaScript v Node 22, serverový procesor Intel Xeon 2,8 GHz v cloudu). Pokus je jedna maska a jedno plnění. „Úspěšné plnění“ znamená, že CSP našel řešení do 50 000 kroků.</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Mřížka</th>
<th align="right">Maska</th>
<th align="right">Plnění</th>
<th align="right">Úspěšné plnění</th>
<th align="right">Slov v mřížce</th>
<th align="right">Vtipů na mřížku</th>
</tr>
</thead>
<tbody><tr>
<td>8×8</td>
<td align="right">6 ms</td>
<td align="right">0,50 s</td>
<td align="right">5 %</td>
<td align="right">21</td>
<td align="right">0 (ze 3 mřížek)</td>
</tr>
<tr>
<td>10×10</td>
<td align="right">0,16 s</td>
<td align="right">0,86 s</td>
<td align="right">20 %</td>
<td align="right">33</td>
<td align="right">26</td>
</tr>
<tr>
<td>12×12</td>
<td align="right">0,25 s</td>
<td align="right">1,45 s</td>
<td align="right">43 %</td>
<td align="right">52</td>
<td align="right">86</td>
</tr>
<tr>
<td>15×15</td>
<td align="right">0,32 s</td>
<td align="right">1,82 s</td>
<td align="right">13 %</td>
<td align="right">80</td>
<td align="right">424</td>
</tr>
<tr>
<td>18×18</td>
<td align="right">0,52 s</td>
<td align="right">1,82 s</td>
<td align="right">13 %</td>
<td align="right">118</td>
<td align="right">725</td>
</tr>
</tbody></table>
</div>
<p>Co z měření plyne:</p>
<ul>
<li><strong>Malá mřížka není snadná.</strong> V 8×8 je hodně krátkých slotů s velkým počtem křížení a málo volnosti, plnění uspěje jen v 5 % pokusů. Jedna vyplněná mřížka stojí v průměru asi 10 sekund procesorového času u 8×8, 4 sekundy u 12×12 a 18 sekund u 18×18.</li>
<li><strong>Většina pokusů skončí neúspěchem.</strong> Pokusy proto spouštějte paralelně na všech jádrech a berte první dost dobrý výsledek.</li>
<li><strong>V telefonu to nedává smysl.</strong> Hráč nechce při otevření obrazovky čekat desítky sekund a aplikace by musela nést celý slovník s legendami. Generátor v aplikaci navíc musí často pouštět ke slovu UI, jinak aplikace zamrzá.</li>
</ul>
<h2 id="generovani-ve-velkem"><a class="heading-anchor" href="#generovani-ve-velkem" aria-hidden="true" tabindex="-1">#</a>Generování ve velkém</h2>
<p>Generátor proto patří na počítač s vícejádrovým procesorem. U nás má každé jádro vlastní vlákno se slovníkem načteným jednou při startu. Mřížka je hotová, když některé vlákno vrátí výsledek s dostatečně malým počtem nevyužitých políček. Nepřetržitý režim generuje dokola jednu křížovku z každé velikosti v každém jazyce. Takto vzniklo přes 14 000 křížovek ve čtyřech jazycích (slovenština, čeština, angličtina, němčina) a jedenácti velikostech od 8×8 po 18×18. Každý vtip je v dané velikosti a jazyce použitý nejvýš jednou.</p>
<p>Při takovém objemu rozhodují dvě věci:</p>
<ul>
<li><strong>Formát.</strong> Úplný zápis má kolem 28 kB na křížovku a celý korpus 390 MB. Většina se dá dopočítat (souřadnice, typy políček, čísla legend). Kompaktní zápis s mřížkou jako jedním řetězcem má kolem 2 kB, celý korpus 15 MB a v instalačním balíčku aplikace asi 7 MB.</li>
<li><strong>Měření kvality.</strong> Podíl legend bez slova a mřížek s ostrovy měřte po každém generování a nechte kontrolu selhat, pokud se čísla zhorší.</li>
</ul>
<h2 id="co-reseni-nepokryva"><a class="heading-anchor" href="#co-reseni-nepokryva" aria-hidden="true" tabindex="-1">#</a>Co řešení nepokrývá</h2>
<ul>
<li><strong>Legendy bez slov.</strong> Průměrná křížovka má pořád 2,7 legendy, které nikam neukazují. Levý horní roh je takový vždy.</li>
<li><strong>Slovník bez frekvencí.</strong> Německý slovník z Wiktionary má u většiny slov jedinou legendu, takže řazení podle počtu legend skoro nerozlišuje a do mřížky se dostávají vzácná slova. Pomohl by frekvenční slovník, zatím ho nemáme.</li>
<li><strong>Tajenka není pohromadě.</strong> Kdo je zvyklý na tajenky z novin, může ji hledat v řádku. Je to kompromis, ne ideál.</li>
</ul>
<p>Pokud řešíte podobný problém s generováním obsahu nebo potřebujete mobilní aplikaci, <a href="https://smallwater.eu/cs/#kontakt">napište nám</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak vygenerovat 3D model telefonu v prohlížeči</title>
      <link>https://smallwater.eu/cs/blog/threejs-csg-3d-phone-model/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/threejs-csg-3d-phone-model/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Ecranify</category>
      <description>Návod na 3D model telefonu v prohlížeči přes Three.js a CSG: tělo s bevel hranou, výřezy tlačítek a reproduktorů, Web Worker a rychlé odečtení otvorů.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/cover.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/cover.jpg" alt="Dva vygenerované 3D modely telefonu, jeden zepředu s tmavým displejem a druhý zezadu" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#co-budete-potrebovat" data-ga="blog_toc_clicked" data-ga-heading-id="co-budete-potrebovat">Co budete potřebovat</a></li><li><a href="#krok-1-vytahnete-obdelnik-do-3d" data-ga="blog_toc_clicked" data-ga-heading-id="krok-1-vytahnete-obdelnik-do-3d">Krok 1: Vytáhněte obdélník do 3D</a></li><li><a href="#krok-2-vyriznete-vanu-pro-displej" data-ga="blog_toc_clicked" data-ga-heading-id="krok-2-vyriznete-vanu-pro-displej">Krok 2: Vyřízněte vanu pro displej</a></li><li><a href="#krok-3-bocni-prvky-pripravte-jako-vrtaky" data-ga="blog_toc_clicked" data-ga-heading-id="krok-3-bocni-prvky-pripravte-jako-vrtaky">Krok 3: Boční prvky připravte jako vrtáky</a></li><li><a href="#krok-4-predni-detaily-nevrtejte" data-ga="blog_toc_clicked" data-ga-heading-id="krok-4-predni-detaily-nevrtejte">Krok 4: Přední detaily nevrtejte</a></li><li><a href="#krok-5-presunte-vypocet-do-web-workeru" data-ga="blog_toc_clicked" data-ga-heading-id="krok-5-presunte-vypocet-do-web-workeru">Krok 5: Přesuňte výpočet do Web Workeru</a></li><li><a href="#krok-6-odectete-vsechny-otvory-najednou" data-ga="blog_toc_clicked" data-ga-heading-id="krok-6-odectete-vsechny-otvory-najednou">Krok 6: Odečtěte všechny otvory najednou</a></li><li><a href="#krok-7-pocet-segmentu-mejte-jako-parametr" data-ga="blog_toc_clicked" data-ga-heading-id="krok-7-pocet-segmentu-mejte-jako-parametr">Krok 7: Počet segmentů mějte jako parametr</a></li><li><a href="#render-na-serveru-bez-graficke-karty" data-ga="blog_toc_clicked" data-ga-heading-id="render-na-serveru-bez-graficke-karty">Render na serveru bez grafické karty</a></li></ol></nav><p>3D model telefonu vygenerujete v prohlížeči tak, že obdélník se zaoblenými rohy vytáhnete do hloubky a pak z něj booleovskými operacemi (CSG) vyříznete přední stranu, tlačítka, reproduktory a konektor. Celé zařízení pak popisuje pár čísel v milimetrech a nový model znamená změnit tato čísla, ne modelovat ve 3D programu.</p>
<p>Hodí se to všude, kde potřebujete hodně podobných zařízení nebo je má upravovat uživatel: konfigurátory, mockupy, náhledy produktů. Jako příklad z praxe slouží <a href="https://ecranify.com" target="_blank" rel="noopener">Ecranify</a>, nástroj na 3D mockupy pro screenshoty do App Store a Google Play, ve kterém není ani jeden hotový 3D model. Níže je postup krok za krokem i s měřením, které ukazuje, kde se dá na výpočtu ušetřit nejvíc.</p>
<h2 id="co-budete-potrebovat"><a class="heading-anchor" href="#co-budete-potrebovat" aria-hidden="true" tabindex="-1">#</a>Co budete potřebovat</h2>
<ul>
<li><a href="https://threejs.org" target="_blank" rel="noopener">Three.js</a> na scénu, kamery, světla a hlavně geometrii. Geometrie je v Three.js mesh, tedy síť trojúhelníků.</li>
<li>Knihovnu <a href="https://github.com/gkjohnson/three-bvh-csg" target="_blank" rel="noopener">three-bvh-csg</a> na booleovské operace nad meshi. Zrychluje je prostorovým indexem (BVH, bounding volume hierarchy), díky kterému netestuje každý trojúhelník proti každému.</li>
<li>Popis zařízení jako data, například JSON s rozměry těla, poloměrem rohů, velikostí bevelu a seznamem prvků na bocích. Všechny polohy zadávejte v milimetrech od středu zařízení.</li>
</ul>
<h2 id="krok-1-vytahnete-obdelnik-do-3d"><a class="heading-anchor" href="#krok-1-vytahnete-obdelnik-do-3d" aria-hidden="true" tabindex="-1">#</a>Krok 1: Vytáhněte obdélník do 3D</h2>
<p>Nakreslete 2D obdélník se zaoblenými rohy ve skutečných rozměrech, například 163,4 × 78 mm, a vytáhněte ho (extruze) do hloubky 8,75 mm. Dostanete desku s ostrými hranami. Žádný telefon takové nemá, proto při vytahování zapněte i bevel, tedy zaoblení hrany. Three.js dělá obojí v jednom kroku.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Jen extruze</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/slab-sharp.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/slab-sharp.jpg" alt="Hranatá deska bez zaoblených rohů a hran" width="1200" height="800" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">+ rohy a bevel</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/slab-bevel.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/slab-bevel.jpg" alt="Stejná deska se zaoblenými rohy i hranami" width="1200" height="800" loading="lazy" decoding="async" /></picture></div></div><figcaption>Stejný tvar, jiná dvě čísla: poloměr rohu a velikost bevelu.</figcaption></figure>
<p>Pozor na jednu věc: bevel se přidává směrem ven. Pokud chcete přesný vnější rozměr, zmenšete 2D tvar i hloubku o velikost bevelu.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">b = min(bevel, hloubka / 2)
tvar = zaoblený obdélník(šířka - 2b, výška - 2b, poloměr rohu - b)
tělo = vytáhni(tvar, hloubka - 2b, bevel b)</code></pre>
<p>Hladkost oblouků (teselaci) určuje počet segmentů, na které se oblouk rozdělí. Víc segmentů znamená hladší tvar a víc trojúhelníků. Toto číslo později rozhodne o rychlosti celého výpočtu, proto se k němu vrátíme v kroku 7.</p>
<h2 id="krok-2-vyriznete-vanu-pro-displej"><a class="heading-anchor" href="#krok-2-vyriznete-vanu-pro-displej" aria-hidden="true" tabindex="-1">#</a>Krok 2: Vyřízněte vanu pro displej</h2>
<p>CSG (constructive solid geometry) skládá tělesa z jednodušších těles pomocí tří operací: sjednocení, rozdíl a průnik. Rozdíl funguje jako vrták: jedno těleso odebere materiál z druhého.</p>
<p>Displej telefonu není zarovnaný s rámem, sedí v mělké vaně. Tu vyrobíte tak, že uděláte druhou, o 1,4 mm menší kopii těla, necháte z ní jen přední část silnou 0,7 mm (průnik s velkým kvádrem) a tuto část odečtete od těla. Do vzniklé vany pak vložíte tmavou plochu displeje.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Řezné těleso</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/pocket-cutter.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/pocket-cutter.jpg" alt="Tělo telefonu s modrým průsvitným řezným tělesem na přední straně" width="1200" height="800" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Po odečtení</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/pocket-carved.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/pocket-carved.jpg" alt="Tělo s vyříznutou vanou a tmavým displejem uvnitř" width="1200" height="800" loading="lazy" decoding="async" /></picture></div></div><figcaption>Modré těleso je to, co se odečte. Vpravo už je ve vaně displej.</figcaption></figure>
<p>Dva praktické detaily:</p>
<ul>
<li><strong>Kvádr pro průnik musí být opravdu velký.</strong> Stačí mu ořezávat hloubku, ale v šířce a výšce musí přesahovat celé zařízení. Neodvozujte ho od hloubky: například 20násobek hloubky je u tenkého vysokého telefonu méně než jeho výška a nahoře a dole zůstane neořezaný rám.</li>
<li><strong>Zvětšete řezné těleso o pár promile.</strong> Když mají dvě tělesa přesně stejnou plochu, CSG si neví rady, která strana je uvnitř. Měřítko 1,002 tomu předejde a okem ho nevidíte.</li>
</ul>
<h2 id="krok-3-bocni-prvky-pripravte-jako-vrtaky"><a class="heading-anchor" href="#krok-3-bocni-prvky-pripravte-jako-vrtaky" aria-hidden="true" tabindex="-1">#</a>Krok 3: Boční prvky připravte jako vrtáky</h2>
<p>Každý prvek na boku je stejný recept s jiným vrtákem. Tlačítko například vznikne ze tří těles: zaoblený obdélník o kousek hlubší než tlačítko se odečte od rámu, okraj otvoru dostane malé zkosení a do otvoru se vloží o něco menší těleso, které z rámu trochu vyčnívá.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Vrtáky</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/button-cutters.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/button-cutters.jpg" alt="Bok telefonu se dvěma modrými řeznými tělesy tlačítek" width="1200" height="800" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Výřez a tlačítko</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/button-cut.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/button-cut.jpg" alt="Bok telefonu s vyříznutými otvory a vloženými tlačítky" width="1200" height="800" loading="lazy" decoding="async" /></picture></div></div><figcaption>Dvě tlačítka na pravém boku před odečtením a po něm.</figcaption></figure>
<p>Reproduktory jsou řada malých válců, konektor je zaoblený obdélník. Do každého otvoru vložte něco tmavého: tenký disk do mřížky, destičku do konektoru. Prázdná díra v tenké skořepině působí jako chyba, díra s tmavou výplní působí jako hardware.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/bottom-edge.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/bottom-edge.jpg" alt="Spodní hrana telefonu se dvěma řadami otvorů reproduktoru a konektorem uprostřed" width="1200" height="800" loading="lazy" decoding="async" /></picture><figcaption>Otvory reproduktoru a konektor. Všechno jsou odečtené válce a zaoblené obdélníky.</figcaption></figure>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">pro každý prvek v seznamu prvků boku:
    vrták = tvar prvku (válec, zaoblený obdélník, ...)
    umísti vrták na hranu podle polohy v mm
    přidej vrták do seznamu řezů
    pokud má prvek výplň: přidej výplň do scény</code></pre>
<p>Přidat tlačítko je pak zápis do dat. Prvků rychle přibývá: iPad Pro 13″ má 58 otvorů reproduktoru, 4 tlačítka, 2 antény a konektor, tedy přes 60 odečtení od jednoho těla.</p>
<h2 id="krok-4-predni-detaily-nevrtejte"><a class="heading-anchor" href="#krok-4-predni-detaily-nevrtejte" aria-hidden="true" tabindex="-1">#</a>Krok 4: Přední detaily nevrtejte</h2>
<p>Výřez pro kameru, ostrůvek v horní části displeje nebo indikátor domovské obrazovky jsou ploché. Řezat pro ně geometrii je zbytečná práce. Nalepte je jako decal, tedy obrázek promítnutý na povrch. Promítejte ho na skrytou rovinu přes celou přední stranu a polohu zadávejte ve stejných milimetrech jako všechno ostatní. Výjimkou je kulaté domovské tlačítko starších iPhonů. To má skutečnou hloubku, kterou je vidět z boku, a proto jde přes CSG.</p>
<aside class="app-box app-box-callout"><img class="app-box-img" src="https://smallwater.eu/images/ecranify/home.png" alt="" width="1400" height="1219" loading="lazy" decoding="async" /><div class="app-box-body"><p class="app-box-eyebrow">Vyzkoušejte</p><h2 class="app-box-title">Ecranify</h2><p>Nástroj v prohlížeči pro fotorealistické 3D mockupy zařízení pro screenshoty do App Store a Google Play. Poznámky k WebGL a novinky.</p><div class="app-box-links"><a class="btn btn-primary" href="https://ecranify.com" target="_blank" rel="noopener" data-ga="blog_app_link_clicked" data-ga-category="ecranify" data-ga-destination="https://ecranify.com" data-ga-app="ecranify" data-ga-store="web">ecranify.com</a></div></div></aside>
<h2 id="krok-5-presunte-vypocet-do-web-workeru"><a class="heading-anchor" href="#krok-5-presunte-vypocet-do-web-workeru" aria-hidden="true" tabindex="-1">#</a>Krok 5: Přesuňte výpočet do Web Workeru</h2>
<p>JavaScript v prohlížeči běží v jednom vlákně. Totéž vlákno počítá, kreslí stránku a reaguje na kliknutí. Dokud běží CSG, stránka stojí a nepomůže ani spinner, protože i ten potřebuje stejné vlákno.</p>
<p>Celé sestavení zařízení proto spusťte ve <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API" target="_blank" rel="noopener">Web Workeru</a>, tedy v samostatném vlákně na pozadí. Worker dostane popis zařízení, vrátí hotová pole vrcholů a stránka zůstane plynulá. Při změně parametru starý worker ukončete a spusťte nový, ať nedopočítává zastaralý stav.</p>
<p>Výsledek z workeru chvíli trvá, proto přidejte okamžitý náhled. Upravovaný prvek ukažte jako modrého průsvitného „ducha“, jednoduchý kvádr nebo válec s pár desítkami trojúhelníků, který jde postavit v hlavním vlákně hned. Když worker skončí, duch zmizí.</p>
<h2 id="krok-6-odectete-vsechny-otvory-najednou"><a class="heading-anchor" href="#krok-6-odectete-vsechny-otvory-najednou" aria-hidden="true" tabindex="-1">#</a>Krok 6: Odečtěte všechny otvory najednou</h2>
<p>Nejpřímočařejší způsob je odečítat prvky postupně: otvor 1 od těla, pak otvor 2 od výsledku a tak dále. Každé odečtení ale znovu postaví prostorový index celého těla, což je nejdražší část výpočtu. Při desítkách prvků čas roste velmi rychle.</p>
<p>Lepší je nejdřív posbírat všechny vrtáky pro dané těleso, spojit je do jednoho meshe a odečíst jednou.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">řezy = []
pro každý prvek: přidej jeho vrták do řezy
spojený = spoj všechny řezy do jednoho meshe
tělo = tělo mínus spojený
pokud spojení selže: odečti řezy po jednom</code></pre>
<p>Měření na zjednodušené kopii postupu (Three.js 0.170, three-bvh-csg 0.0.17, Node 22, cloudový server se 2 jádry Intel Xeon 2,1 GHz, medián z 5 běhů):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Těleso</th>
<th>Otvory</th>
<th>Po jednom</th>
<th>Najednou</th>
</tr>
</thead>
<tbody><tr>
<td>telefon, 17 676 trojúhelníků</td>
<td>25</td>
<td>2,47 s</td>
<td>0,26 s</td>
</tr>
<tr>
<td>tablet, 17 676 trojúhelníků</td>
<td>58</td>
<td>5,55 s</td>
<td>0,89 s</td>
</tr>
<tr>
<td>tablet, 68 108 trojúhelníků</td>
<td>58</td>
<td>22,8 s</td>
<td>3,1 s</td>
</tr>
</tbody></table>
</div>
<p>Jedna podmínka: spojení a jedno odečtení dá stejný výsledek jako postupné odečítání, jen pokud se vrtáky nepřekrývají. Prvky jsou na různých místech, takže to běžně platí. Pokud ale uživatel posune dva prvky na sebe, v místě překrytí může vzniknout drobná chyba.</p>
<h2 id="krok-7-pocet-segmentu-mejte-jako-parametr"><a class="heading-anchor" href="#krok-7-pocet-segmentu-mejte-jako-parametr" aria-hidden="true" tabindex="-1">#</a>Krok 7: Počet segmentů mějte jako parametr</h2>
<p>Teselace, která stačí na zobrazení, je pro CSG často zbytečně hustá. Při 64 segmentech na roh a 32 na bevel má tělo 68 108 trojúhelníků. Polovinu (32 a 16) na 2mm hraně nevidíte, ale tělo má jen 17 676 trojúhelníků a třetí řádek tabulky ukazuje, co to dělá s časem.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">64 a 32 segmentů</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/tessellation-64.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/tessellation-64.png" alt="Drátový model rohu telefonu s velmi hustou sítí trojúhelníků" width="1200" height="800" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">32 a 16 segmentů</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/threejs-csg-3d-phone-model/tessellation-32.webp" /><img src="https://smallwater.eu/blog/threejs-csg-3d-phone-model/tessellation-32.png" alt="Drátový model stejného rohu s polovičním počtem segmentů" width="1200" height="800" loading="lazy" decoding="async" /></picture></div></div><figcaption>Roh těla shora. Vpravo je čtvrtina trojúhelníků.</figcaption></figure>
<p>Malým prvkům s poloměrem do 2 mm (otvory reproduktoru, šroubky) dejte polovinu segmentů, náhledům v galerii ještě méně. Kvalitu proto nedávejte jako konstantu do kódu, ale jako parametr sestavení.</p>
<h2 id="render-na-serveru-bez-graficke-karty"><a class="heading-anchor" href="#render-na-serveru-bez-graficke-karty" aria-hidden="true" tabindex="-1">#</a>Render na serveru bez grafické karty</h2>
<p>Pokud potřebujete stejnou scénu vykreslit i na serveru, například jako zálohu exportu, běží v Chromu bez obrazovky (headless), který bez grafické karty kreslí softwarově. Tam se každá neefektivita znásobí. V Ecranify nejtěžší tablet bez zmíněných úprav narazil na limit 15 minut. Se spojenými řezy, menšími texturami, nižší teselací a cache hotové geometrie podle hashe popisu zařízení trvá render kolem půl minuty a opakovaný kolem 5 sekund. Podrobnosti i s interaktivními ukázkami jsou v <a href="https://ecranify.com/guides/how-we-build-3d-device-mockups" target="_blank" rel="noopener">anglickém článku na ecranify.com</a>.</p>
<p>Pokud řešíte podobný problém s 3D v prohlížeči nebo potřebujete aplikaci na zakázku, <a href="https://smallwater.eu/cs/#kontakt">napište nám</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak převést projekt z Unity 4 do Unity 6</title>
      <link>https://smallwater.eu/cs/blog/unity-4-to-unity-6-upgrade/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/unity-4-to-unity-6-upgrade/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Onager</category>
      <description>Jak dostat projekt z Unity 4 do Unity 6 bez ztráty scén: převod na text přes Unity 2019.4, port UnityScriptu do C# se zachováním GUID a obnova starého HUD.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/cover.webp" /><img src="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/cover.jpg" alt="Stavba z bloků v levelu Onageru na podlaze z mléčného skla" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#co-se-stane-kdyz-projekt-otevrete-v-unity-6" data-ga="blog_toc_clicked" data-ga-heading-id="co-se-stane-kdyz-projekt-otevrete-v-unity-6">Co se stane, když projekt otevřete v Unity 6</a></li><li><a href="#krok-1-zaznamenejte-puvodni-stav" data-ga="blog_toc_clicked" data-ga-heading-id="krok-1-zaznamenejte-puvodni-stav">Krok 1: Zaznamenejte původní stav</a></li><li><a href="#krok-2-prevod-na-text-pres-unity-2019-4" data-ga="blog_toc_clicked" data-ga-heading-id="krok-2-prevod-na-text-pres-unity-2019-4">Krok 2: Převod na text přes Unity 2019.4</a></li><li><a href="#krok-3-vysledek-overte-v-editoru-ne-ctenim-yaml" data-ga="blog_toc_clicked" data-ga-heading-id="krok-3-vysledek-overte-v-editoru-ne-ctenim-yaml">Krok 3: Výsledek ověřte v editoru, ne čtením YAML</a></li><li><a href="#krok-4-unityscript-do-c-se-stejnym-guid" data-ga="blog_toc_clicked" data-ga-heading-id="krok-4-unityscript-do-c-se-stejnym-guid">Krok 4: UnityScript do C# se stejným GUID</a></li><li><a href="#krok-5-obnovte-hud-z-puvodnich-binarnich-scen" data-ga="blog_toc_clicked" data-ga-heading-id="krok-5-obnovte-hud-z-puvodnich-binarnich-scen">Krok 5: Obnovte HUD z původních binárních scén</a></li><li><a href="#po-prevodu-stare-triky-nahradte" data-ga="blog_toc_clicked" data-ga-heading-id="po-prevodu-stare-triky-nahradte">Po převodu: staré triky nahraďte</a></li><li><a href="#co-postup-nevyresi" data-ga="blog_toc_clicked" data-ga-heading-id="co-postup-nevyresi">Co postup nevyřeší</a></li></ol></nav><p>Projekt z Unity 4 neotevírejte přímo v Unity 6 a hlavně v něm nic neukládejte. Unity 6 staré binární scény načte jen částečně a uložením ztrátu potvrdí; bezpečná cesta vede přes Unity 2019.4, které starý formát umí přečíst i zapsat jako text, a teprve potom do Unity 6.</p>
<p>Týká se to projektů, které mají scény a prefaby uložené v binárním formátu a skripty v UnityScriptu, jazyce podobném JavaScriptu, který Unity kdysi podporovalo vedle C#. Postup ukážeme na hře Onager, fyzikální hře s katapultem napsané v Unity 4.1.2 a v UnityScriptu. Projekt měl 126 skriptů, 75 scén a 78 prefabů, po převodu běží na Unity 6000.5 s URP (Universal Render Pipeline) a je zdarma na <a href="https://smallwater.eu/onager-ios">App Store</a> i <a href="https://smallwater.eu/onager-android">Google Play</a>.</p>
<h2 id="co-se-stane-kdyz-projekt-otevrete-v-unity-6"><a class="heading-anchor" href="#co-se-stane-kdyz-projekt-otevrete-v-unity-6" aria-hidden="true" tabindex="-1">#</a>Co se stane, když projekt otevřete v Unity 6</h2>
<p>Nejjednodušší přístup je otevřít projekt v Unity 6 a přepnout serializaci na Force Text, tedy na ukládání assetů jako čitelný YAML. To ale není překlad souborů. Unity každý asset načte přes svůj importér a zapíše zpět jako YAML, takže všechno projde loaderem Unity 6. Ten u každé scény hlásí, že našel Transform bez GameObjectu, a „visící“ komponentu smaže. Uložení to udělá natrvalo.</p>
<p>Rozsah ztráty se dá změřit nástrojem binary2text, který je přiložený k editoru a čte serializovaný soubor bez importéru. V Onageru jsme jeho výpis porovnali s výsledným YAML a počítali jen komponenty, které zmizely z GameObjectů, jež přežily:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Komponenta</th>
<th align="right">Ztraceno</th>
</tr>
</thead>
<tbody><tr>
<td>Camera</td>
<td align="right">130</td>
</tr>
<tr>
<td>ParticleSystem</td>
<td align="right">132</td>
</tr>
<tr>
<td>Transform</td>
<td align="right">167</td>
</tr>
<tr>
<td>BoxCollider</td>
<td align="right">104</td>
</tr>
<tr>
<td>Rigidbody</td>
<td align="right">100</td>
</tr>
<tr>
<td>GUITexture / GUIText</td>
<td align="right">303 / 179</td>
</tr>
</tbody></table>
</div>
<p>Bez ztráty nebyla ani jedna ze 75 scén. Zmizely celé kamery pro HUD a hlavní kamera dostala novou, výchozí. Důležité je, že škoda vzniká už při načtení, ne při uložení. Dokud projekt není převedený, nesmí ho Unity 6 uložit, ani jednu scénu, ani jeden prefab.</p>
<h2 id="krok-1-zaznamenejte-puvodni-stav"><a class="heading-anchor" href="#krok-1-zaznamenejte-puvodni-stav" aria-hidden="true" tabindex="-1">#</a>Krok 1: Zaznamenejte původní stav</h2>
<p>Než na cokoli sáhnete, vyfoťte si každou scénu a pro každou scénu si nástrojem binary2text uložte výpis původního souboru. Podle výpisu budete v kroku 3 kontrolovat kamery a v kroku 5 obnovovat HUD. Původní binární soubory nechte v historii gitu, nemažte je.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/before-level-1-1.webp" /><img src="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/before-level-1-1.jpg" alt="První level v Unity 6 před remasterem" width="1280" height="720" loading="lazy" decoding="async" /></picture><figcaption>První level Onageru v Unity 6, ještě se starým renderem a původními materiály. Takový záznam si udělejte z každé scény dřív, než na cokoli sáhnete.</figcaption></figure>
<h2 id="krok-2-prevod-na-text-pres-unity-2019-4"><a class="heading-anchor" href="#krok-2-prevod-na-text-pres-unity-2019-4" aria-hidden="true" tabindex="-1">#</a>Krok 2: Převod na text přes Unity 2019.4</h2>
<p>Unity 2019.4 je poslední editor, který formát z Unity 4.1 umí přečíst i zapsat. Převod dělejte se zavřeným editorem v samostatné kopii repozitáře:</p>
<ul>
<li><strong>Verze projektu.</strong> V souboru s verzí projektu nastavte původní verzi (u Onageru 4.1.2), aby editor zvolil cestu pro starý projekt. Unity ji při otevření přepíše, stačí, že platí při prvním importu.</li>
<li><strong>Prázdné balíčky.</strong> Vyprázdněte seznam závislostí balíčků. Jedna chyba kompilace znamená, že nebude fungovat žádná položka menu z editorových skriptů, a to vypadá, jako by se menu vůbec neobjevilo.</li>
<li><strong>Import od nuly.</strong> Smažte složku Library a nechte projekt naimportovat.</li>
<li><strong>Force Text a vynucené přepsání.</strong> Unity 2019.4 po změně na Force Text existující soubory samo nepřepíše. Je potřeba to vyvolat funkcí pro reserializaci assetů, a to jen assetů, ne i jejich meta souborů. Přepsání všech meta souborů způsobí, že Unity 6 potom importuje stále dokola.</li>
</ul>
<p>Jedna past je v gitu. Pokud v nastavení atributů gitu označíte scény jako text s koncem řádku LF, git z binárních scén při ukládání do repozitáře vyhodí bajty, které vypadají jako konec řádku. V Onageru to poškodilo osm scén. Na assety Unity atribut pro konce řádků nedávejte, Unity stejně zapisuje LF.</p>
<h2 id="krok-3-vysledek-overte-v-editoru-ne-ctenim-yaml"><a class="heading-anchor" href="#krok-3-vysledek-overte-v-editoru-ne-ctenim-yaml" aria-hidden="true" tabindex="-1">#</a>Krok 3: Výsledek ověřte v editoru, ne čtením YAML</h2>
<p>Čtení převedených souborů vede k sebejistým a nesprávným závěrům. Zkrácené záznamy instancí prefabů vypadají jako ztráta, stejně jako kamera, která se jen přesunula do prefabu. Rozhoduje to, co Unity postaví v paměti. Každou převedenou scénu otevřete a její kamery porovnejte s výpisem originálu z kroku 1.</p>
<p>V Onageru mělo po převodu všech 75 scén stejné kamery i jejich pořadí. Ztratilo se jen to, co žádný dnešní editor nezná: 569 GUITexture, 179 GUIText a staré částicové systémy, které Unity odstranilo v roce 2018.</p>
<h2 id="krok-4-unityscript-do-c-se-stejnym-guid"><a class="heading-anchor" href="#krok-4-unityscript-do-c-se-stejnym-guid" aria-hidden="true" tabindex="-1">#</a>Krok 4: UnityScript do C# se stejným GUID</h2>
<p>Unity <a href="https://blog.unity.com/community/unityscripts-long-ride-off-into-the-sunset" target="_blank" rel="noopener">ukončovalo UnityScript postupně od verze 2017.2</a>, takže ruční port do C# je jediná cesta. Scény odkazují na skripty přes GUID, jedinečný identifikátor, který Unity ukládá do meta souboru každého assetu, a hodnoty polí ukládají podle jména pole. Když to při portu dodržíte, všechny odkazy se připojí samy:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">pro každý skript X v UnityScriptu:
  vytvoř C# skript s třídou přesně jménem X
  zkopíruj GUID z meta souboru starého skriptu do meta souboru nového
  veřejná pole: stejné jméno, stejný typ, i s překlepy
  nic nepřejmenovávej, dokud projekt neběží</code></pre>
<p>V Onageru se takto po portu všech 126 skriptů připojilo 12 935 komponent v 73 scénách a v Inspectoru zůstaly všechny nastavené hodnoty.</p>
<p>Textovým hledáním v projektu se nedá zjistit, které skripty se používají. Binární prefab má GUID uložený jako bajty, ne jako text. V Onageru tak vypadalo nepoužívaných jednadvacet skriptů a všechny byly živé. Dejte si pozor i na pozůstatky starších pokusů o port: jeden prefab dřevěné kostky ukazoval na dva skripty z takového pokusu a jen kvůli němu chybělo 1 140 komponent. Pokud je potřeba v binárním souboru změnit GUID, vyměňte ho přímo v bajtech, šestnáct za šestnáct, aby se soubor neposunul.</p>
<h2 id="krok-5-obnovte-hud-z-puvodnich-binarnich-scen"><a class="heading-anchor" href="#krok-5-obnovte-hud-z-puvodnich-binarnich-scen" aria-hidden="true" tabindex="-1">#</a>Krok 5: Obnovte HUD z původních binárních scén</h2>
<p>GUITexture a GUIText v Unity 6 nejsou a s nimi zmizí celý HUD, který na nich stál. Kreslit ho znovu není potřeba. Původní binární scény z historie gitu binary2text přečte bez ohledu na to, jestli třídy ještě existují. Vytáhněte z nich každý obrázek a text HUD s texturou, barvou, pozicí a zarovnáním a skriptem je vraťte na stejné objekty ve scénách. V Onageru to bylo 577 obrázků a 180 textů.</p>
<p>Takto se zachrání i texty, které nikde jinde nejsou. V Onageru to bylo 111 textů: všechny titulky tutoriálů a celá tabule s kredity. Při psaní HUD od nuly byste si je museli vymyslet. Při přenosu barvy počítejte s jednou věcí, kterou dokumentace neříká: staré GUITexture barvu při vykreslení zdvojnásobovalo. Všech 577 obrázků mělo barvu přesně 0,5, takže šedá znamenala bílou.</p>
<h2 id="po-prevodu-stare-triky-nahradte"><a class="heading-anchor" href="#po-prevodu-stare-triky-nahradte" aria-hidden="true" tabindex="-1">#</a>Po převodu: staré triky nahraďte</h2>
<p>Staré projekty často obcházely omezení tehdejšího Unity a tyto obezličky se po převodu vyplatí předělat:</p>
<ul>
<li><strong>Odraz v podlaze.</strong> Onager měl lesklou podlahu bez skutečného odrazu. Pod podlahou stál druhý, zrcadlově otočený svět: kopie každého objektu, jejíž pozici skript každý třetí snímek zkopíroval z originálu, v jednom levelu 50 až 70 klonů. Nahradit se to dá jednou kamerou navíc, která odraz kreslí v polovičním rozlišení. Odrážejí se pak i částice a trosky, což klonovaný svět neuměl.</li>
<li><strong>Míření.</strong> Pokud náhled dráhy předpovídá, kam střelu pošle síla přidaná v prvním fyzikálním kroku, střela dopadne jinam než značka. Spolehlivější je vypočítat rychlost jednou, náhled nakreslit přesně z ní a střele ji přiřadit přímo.</li>
<li><strong>Výkon.</strong> Měřte na slabém telefonu. Na Motorole G52 klesl Onager z 20 na 6 fps jen kvůli shaderu stěn, víc než rozlišení, stíny a odraz dohromady. Na nízkých a středních detailech proto mají stěny jednodušší materiál.</li>
</ul>
<figure class="post-figure post-figure-video"><video class="post-video" muted loop playsinline controls preload="none" poster="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/gravity-shot.jpg" width="720" height="720" data-lazy-video aria-label="Gravitační střela v převedeném Onageru: koule zvedne bloky, kolem ní krouží vír a modré světlo."><source src="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/gravity-shot.mp4" type="video/mp4" /></video><figcaption>Gravitační střela v převedeném Onageru: koule zvedne bloky, kolem ní krouží vír a modré světlo.</figcaption></figure>
<aside class="app-box app-box-callout"><img class="app-box-img" src="https://smallwater.eu/images/onager/1-aim.jpg" alt="" width="1600" height="736" loading="lazy" decoding="async" /><div class="app-box-body"><p class="app-box-eyebrow">Vyzkoušejte</p><h2 class="app-box-title">Onager</h2><p>3D fyzikální hlavolam s katapultem: 60 levelů boření. Fyzika, level design a novinky z vydání.</p><div class="app-box-links"><a class="btn btn-primary" href="https://smallwater.eu/onager-ios" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="onager" data-ga-destination="/onager-ios" data-ga-app="onager" data-ga-store="app_store">App Store</a><a class="btn btn-ghost" href="https://smallwater.eu/onager-android" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="onager" data-ga-destination="/onager-android" data-ga-app="onager" data-ga-store="google_play">Google Play</a></div></div></aside>
<figure class="post-figure post-figure-video"><video class="post-video" muted loop playsinline controls preload="none" poster="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/rainbow-shot.jpg" width="720" height="720" data-lazy-video aria-label="Duhová střela: bílé body vycházejí přímo z povrchu zasaženého bloku."><source src="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/rainbow-shot.mp4" type="video/mp4" /></video><figcaption>Duhová střela: bílé body vycházejí přímo z povrchu zasaženého bloku.</figcaption></figure>
<p>Pokud řešíte fyziku tisíců objektů v současném Unity, podívejte se i na <a href="https://smallwater.eu/cs/blog/fake-ragdoll-unity-dots/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/unity-4-to-unity-6-upgrade/" data-ga-destination="/cs/blog/fake-ragdoll-unity-dots/" data-ga-placement="inline">falešný ragdoll pro tisíce nepřátel v Unity DOTS</a>. Jak po převodu přidat staré 2D hře glow a 2D světla v URP, popisuje <a href="https://smallwater.eu/cs/blog/dreamica-remaster-unity-urp-glow/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/unity-4-to-unity-6-upgrade/" data-ga-destination="/cs/blog/dreamica-remaster-unity-urp-glow/" data-ga-placement="inline">samostatný návod</a>.</p>
<h2 id="co-postup-nevyresi"><a class="heading-anchor" href="#co-postup-nevyresi" aria-hidden="true" tabindex="-1">#</a>Co postup nevyřeší</h2>
<p>Komponenty, které dnešní Unity nezná, se nepřipojí. GUITexture a GUIText je třeba obnovit jako v kroku 5, staré částicové systémy se ztratí. Jména tříd a polí zůstanou přesně jako v původním projektu, i s překlepy a v jazyce, ve kterém je někdo kdysi napsal. Scény se na ně vážou jménem, takže přejmenování má smysl dělat jen přes editor, který odkazy přepíše sám.</p>
<p>Pokud máte starý projekt v Unity a potřebujete ho dostat do současné verze, <a href="https://smallwater.eu/cs/#kontakt">napište nám</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak navigovat hordu tisíců nepřátel v Unity DOTS</title>
      <link>https://smallwater.eu/cs/blog/unity-dots-horde-navigation/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/unity-dots-horde-navigation/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Tower Defense</category>
      <description>Návod na pohyb tisíců nepřátel v Unity DOTS: flow field, odtlačování od sousedů a stěn, cena podle hustoty davu, zácpy v úžinách a výkon simulace.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/cover.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/cover.jpg" alt="Horda nepřátel jako světlé tečky obtéká překážky k cíli, pod ní šipky flow fieldu a červená mapa hustoty davu" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#krok-1-flow-field-misto-cesty-pro-kazdeho" data-ga="blog_toc_clicked" data-ga-heading-id="krok-1-flow-field-misto-cesty-pro-kazdeho">Krok 1: Flow field místo cesty pro každého</a></li><li><a href="#krok-2-samostatne-pole-pro-kazdou-velikost-nepritele" data-ga="blog_toc_clicked" data-ga-heading-id="krok-2-samostatne-pole-pro-kazdou-velikost-nepritele">Krok 2: Samostatné pole pro každou velikost nepřítele</a></li><li><a href="#krok-3-odtlacovani-od-sousedu" data-ga="blog_toc_clicked" data-ga-heading-id="krok-3-odtlacovani-od-sousedu">Krok 3: Odtlačování od sousedů</a></li><li><a href="#krok-4-odtlacovani-od-sten" data-ga="blog_toc_clicked" data-ga-heading-id="krok-4-odtlacovani-od-sten">Krok 4: Odtlačování od stěn</a></li><li><a href="#krok-5-cena-podle-hustoty-davu-a-zpomaleni" data-ga="blog_toc_clicked" data-ga-heading-id="krok-5-cena-podle-hustoty-davu-a-zpomaleni">Krok 5: Cena podle hustoty davu a zpomalení</a></li><li><a href="#krok-6-zacpa-v-uzine-cekat-ne-tlacit" data-ga="blog_toc_clicked" data-ga-heading-id="krok-6-zacpa-v-uzine-cekat-ne-tlacit">Krok 6: Zácpa v úžině: čekat, ne tlačit</a></li><li><a href="#krok-7-predjizdeni-velkych-nepratel" data-ga="blog_toc_clicked" data-ga-heading-id="krok-7-predjizdeni-velkych-nepratel">Krok 7: Předjíždění velkých nepřátel</a></li><li><a href="#krok-8-sloty-u-cile-utoku" data-ga="blog_toc_clicked" data-ga-heading-id="krok-8-sloty-u-cile-utoku">Krok 8: Sloty u cíle útoku</a></li><li><a href="#vykon-simulace-a-vykreslovani" data-ga="blog_toc_clicked" data-ga-heading-id="vykon-simulace-a-vykreslovani">Výkon simulace a vykreslování</a></li><li><a href="#limity-reseni" data-ga="blog_toc_clicked" data-ga-heading-id="limity-reseni">Limity řešení</a></li></ol></nav><p>Hordu tisíců nepřátel v Unity DOTS navigujte jedním flow fieldem pro celou mapu a několika levnými lokálními pravidly: odtlačování od sousedů a od stěn, cena cesty podle hustoty davu a stav „čekám“ v zácpě. V Unity Editoru na MacBooku s M3 Pro to drží kolem 9 600 nepřátel najednou při 90 FPS.</p>
<p>Problém vzniká v tower defense, strategických a survival hrách, kde k cíli jdou naráz tisíce jednotek. Klasické hledání cesty pro každou zvlášť je příliš drahé a bez lokálních pravidel se dav překrývá, lepí na stěny a zasekává v úžinách. Unity DOTS (Data-Oriented Technology Stack, tedy Entities, ECS a Job System) dává výkon na tisíce entit, samotnou navigaci ale neřeší. Postup níže používáme v naší tower defense hře pro PC (je v <a href="https://smallwater.eu/cs/#referencie">referencích</a>). Obrázky jsou ze zjednodušené 2D simulace, která běží podle stejných pravidel, nejsou to záběry ze hry.</p>
<h2 id="krok-1-flow-field-misto-cesty-pro-kazdeho"><a class="heading-anchor" href="#krok-1-flow-field-misto-cesty-pro-kazdeho" aria-hidden="true" tabindex="-1">#</a>Krok 1: Flow field místo cesty pro každého</h2>
<p>Flow field je mřížka přes celou mapu, ve které každá buňka ví, kterým směrem se z ní jde k cíli. Nepřítel nehledá cestu, jen si přečte směr z místa, kde stojí. Cena je stejná pro 10 i 10 000 nepřátel.</p>
<p>Rozdělte mapu na buňky po 1 m (náš testovací level má 128 × 128) a pro každý cíl jednou spočítejte vzdálenost z každé buňky k cíli. Statické pole stačí spočítat v editoru při přípravě levelu a uložit do assetu.</p>
<p>Nejjednodušší verze je Dijkstra přes 8 sousedů a v každé buňce jeden z 8 směrů. Funguje, ale nepřátelé chodí po úhlopříčkách a v řadě se lepí na stěny.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">8 směrů</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/flow-8dir.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/flow-8dir.png" alt="Flow field s osmi směry, dráhy nepřátel vedou po úhlopříčkách a lepí se na hranu překážky" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Plynulé pole</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/flow-smooth.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/flow-smooth.png" alt="Plynulý flow field, dráhy se obloukem sbíhají kolem rohu překážky s odstupem od stěny" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div></div><figcaption>Stejná mapa, dráhy osamocených nepřátel. Vlevo 8 směrů, vpravo plynulé pole s přirážkou u stěn.</figcaption></figure>
<p>Lepší výsledek dává plynulé pole. Vzdálenost spočítejte řešením <a href="https://en.wikipedia.org/wiki/Eikonal_equation" target="_blank" rel="noopener">eikonální rovnice</a> (rovnice šíření vlny, jejímž řešením je nejkratší vzdálenost ve spojitém prostoru, ne jen po hranách mřížky) metodou fast sweeping, která mřížku opakovaně prochází ze čtyř rohů. Směr pak není směr buňky, ale gradient vzdálenosti v přesném místě nepřítele. K ceně buňky připočtěte přirážku za blízkost stěny, takže pole samo drží dav kousek od překážek.</p>
<h2 id="krok-2-samostatne-pole-pro-kazdou-velikost-nepritele"><a class="heading-anchor" href="#krok-2-samostatne-pole-pro-kazdou-velikost-nepritele" aria-hidden="true" tabindex="-1">#</a>Krok 2: Samostatné pole pro každou velikost nepřítele</h2>
<p>Velký nepřítel se nevejde všude tam, kam malý. Zaveďte velikostní třídy: každá má vlastní průchodnost (buňka musí být dost daleko od stěny) a vlastní pole pro každý cíl. Třídy držte jako data levelu, ne v kódu. U nás jsou tři.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/size-classes.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/size-classes.png" alt="Mapa s úzkou chodbou: malí nepřátelé jdou chodbou, větší oranžoví ji obcházejí spodem" width="1792" height="1008" loading="lazy" decoding="async" /></picture><figcaption>Malí nepřátelé jdou úzkou chodbou, velcí (oranžoví) se do ní nevejdou a obcházejí ji.</figcaption></figure>
<h2 id="krok-3-odtlacovani-od-sousedu"><a class="heading-anchor" href="#krok-3-odtlacovani-od-sousedu" aria-hidden="true" tabindex="-1">#</a>Krok 3: Odtlačování od sousedů</h2>
<p>Flow field řekne směr, ale o sousedech neví nic. Základem je zjednodušený model boids (klasická pravidla hejna): každý nepřítel se odpuzuje od sousedů v okruhu 0,9 m. Sousedy hledejte ve spatial hashi, tedy v prostorové mřížce, kde každá buňka drží seznam jednotek v ní. Hash postavte každý tik znovu a seřaďte, aby bylo pořadí deterministické.</p>
<p>Samotné odpuzování nestačí. Při 3 000 nepřátelích mělo 83,7 % z nich nejbližšího souseda blíž než 0,8 m a nejmenší vzdálenost byla 4 cm, tedy dva nepřátelé skoro v sobě.</p>
<p>Vyšší váha odpuzování problém neřeší. Při váze 6 místo 1,5 klesl podíl těsných na 54,6 %, při 12 na 44,3 %, při 24 už nic (48,2 %). Dav navíc začne poskakovat, protože se sčítají neomezené tlaky od mnoha sousedů najednou.</p>
<p>Pomůže poziční korekce. Po pohybu rozsuňte každou překrývající se dvojici, každého o polovinu průniku, a posun za tik omezte na 0,15 m. Bez stropu korekce skáče stejně jako vysoká váha. Se stropem zůstane těsných 60,8 %, ale nejmenší vzdálenost stoupne na 28 cm a dav neposkakuje.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Jen odpuzování</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/overlap-before.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/overlap-before.png" alt="Dav u úžiny, velká část nepřátel je červená, překrývají se" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Korekce překrytí</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/overlap-after.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/overlap-after.png" alt="Stejný dav s korekcí překrytí, červení zůstali jen těsně před úžinou" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div></div><figcaption>Červení se se sousedem překrývají o víc než 20 cm. Na volné ploše korekce pomáhá, před úžinou ne.</figcaption></figure>
<h2 id="krok-4-odtlacovani-od-sten"><a class="heading-anchor" href="#krok-4-odtlacovani-od-sten" aria-hidden="true" tabindex="-1">#</a>Krok 4: Odtlačování od stěn</h2>
<p>U stěny korekce mezi nepřáteli selhává. Pohyb do stěny je zakázaný (posun se rozloží po osách a složka do stěny se zahodí), takže přitlačený nepřítel nemá kam uhnout a korekce od souseda se ztratí. Z 938 překrývajících se nepřátel mělo nenulový tlak od stěny jen 35.</p>
<p>Přidejte proto odtlačování od nejbližších neprůchozích buněk ve 4 směrech i diagonálně. Dvě chyby, kterým se dá vyhnout:</p>
<ul>
<li><strong>Vnitřní roh.</strong> Tlak od obou hran a od rohu se sečte a nepřítele to odhodí. Tlak od rohu přidávejte jen u vnějšího rohu.</li>
<li><strong>Skutečné stěny.</strong> Odtlačujte od skutečných stěn, ne od průchodnosti velikostní třídy. Pro velkého nepřítele je „stěna“ už metr před skutečnou. V ústí chodby tak tlak od rohu vyrovnal pohyb velkého nepřítele dopředu a celá skupina za ním zůstala stát.</li>
</ul>
<h2 id="krok-5-cena-podle-hustoty-davu-a-zpomaleni"><a class="heading-anchor" href="#krok-5-cena-podle-hustoty-davu-a-zpomaleni" aria-hidden="true" tabindex="-1">#</a>Krok 5: Cena podle hustoty davu a zpomalení</h2>
<p>I s plynulým polem jde celý dav po nejkratší cestě těsně kolem rohu, bez ohledu na to, kolik nepřátel tam už je. Řešení nabízí <a href="https://grail.cs.washington.edu/projects/continuum-crowds" target="_blank" rel="noopener">Continuum Crowds</a> (Treuille a kol., 2006): cena buňky roste s hustotou davu. Jednou za 10 tiků spočítejte nepřátele v buňkách, vyhlaďte to a přepočítejte pole jednoho cíle. U přeplněného rohu pak pole pošle nepřítele kolem, přes volnější buňky.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Bez hustoty</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/density-off.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/density-off.png" alt="Dav obchází překážku těsně kolem rohu v úzkém proudu, červená mapa hustoty je soustředěná" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Cena podle hustoty</span><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/density-on.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/density-on.png" alt="Dav obchází překážku širokým obloukem, hustota je rozložená" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div></div><figcaption>Stejný dav kolem stejného rohu. Červená je vyhlazená hustota davu.</figcaption></figure>
<p>Druhá polovina je zpomalení. Nepřítel v husté buňce jde pomaleji, nejméně na 15 % rychlosti. Na 2 250 nepřátelích šli v řídkých buňkách průměrně na 94 % rychlosti, ve středně hustých na 71 % a v zácpě na 20 %. Ať zpomalení začíná dřív: s prahem volné hustoty 0,6 nepřítele na buňku místo 1 klesl podíl těsných v úžině na 36,7 %.</p>
<p>Praktická rada: pole, která vznikají až za běhu (například mapa hustoty), alokujte dřív, než je poprvé použije job. Job s ještě nevytvořeným polem shodí Unity Editor nativně, bez výjimky.</p>
<h2 id="krok-6-zacpa-v-uzine-cekat-ne-tlacit"><a class="heading-anchor" href="#krok-6-zacpa-v-uzine-cekat-ne-tlacit" aria-hidden="true" tabindex="-1">#</a>Krok 6: Zácpa v úžině: čekat, ne tlačit</h2>
<p>Když se dav nemá kam rozšířit, zbývá úžina. Nejjednodušší nápad je zahodit složku rychlosti směrem k sousedovi, kterého se nepřítel dotýká. Zadní pak stojí, místo aby uhnuli, a zácpu udržují.</p>
<p>Uhnout stranou, například o 35° na stranu s méně sousedy, je lepší, ale dav se začne míhat. Vyhlazení natáčení a nižší rychlost otáčení (180 místo 360 °/s) příznak jen tlumí. Příčina je jinde: při vyrovnané hustotě si nepřítel volí stranu každý tik jinak. Pomůže hystereze, tedy zvýhodnění předchozího rozhodnutí: strana z minulého tiku dostane při porovnání slevu 3 sousedů.</p>
<p>Výsledné řešení spojuje kontaktní omezení a tři stavy: jde, obchází, čeká. Složka rychlosti do dotýkajícího se souseda se zahodí, boční zůstane. Kdo nemá volnou stranu ani místo před sebou, čeká a netlačí.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">pro každého nepřítele, každý tik:
    rychlost = směr z flow fieldu × rychlost × zpomalení podle hustoty
    rychlost += odpuzování od sousedů
    pokud se vpředu dotýká souseda:
        pokud je volná strana: obcházej (strana s hysterezí)
        jinak: čekej
    zahoď složku rychlosti do dotýkajících se sousedů
    posuň se, složku do stěny zahoď
pak pro všechny:
    rozsuň překrývající se dvojice (polovina průniku, nejvýš 0,15 m)
    odtlač od stěn</code></pre>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/crowd-states.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/crowd-states.png" alt="Dav před úžinou, bílé tečky jdou, červené čekají, za úžinou jde řada s odstupy" width="1792" height="1008" loading="lazy" decoding="async" /></picture><figcaption>Před úžinou: bílí jdou, červení čekají. Za úžinou jde řada s odstupy.</figcaption></figure>
<p>Na tři věci si dejte pozor:</p>
<ul>
<li><strong>Trpělivost.</strong> Když se potkají dva nepřátelé v protisměru, ať čeká ten s vyšším ID. Řetěz sousedů ho ale může blokovat tisíce tiků, proto po limitu čekání zkusí nouzově uhnout.</li>
<li><strong>Nulová rychlost.</strong> Nepřítel s nulovou požadovanou rychlostí se ze stavu „čeká“ nikdy nevrátí a po vlně zůstane stát na volné ploše. Kdo se nechce hýbat, ať je vždy ve stavu „jde“.</li>
<li><strong>Úzký kužel.</strong> Souseda vpředu hledejte v úzkém kuželu s krátkou sondou. Při 45° posílalo nastavení do čekání nebo obcházení 55 až 60 % davu i na volné ploše, při 30° a kratší sondě 41 %.</li>
</ul>
<h2 id="krok-7-predjizdeni-velkych-nepratel"><a class="heading-anchor" href="#krok-7-predjizdeni-velkych-nepratel" aria-hidden="true" tabindex="-1">#</a>Krok 7: Předjíždění velkých nepřátel</h2>
<p>Různě velké a různě rychlé typy nepřátel přinášejí další případy. Náš velký pomalý nepřítel má poloměr v davu 1,25 m a jde 1,1 m/s. Rychlý nepřítel za ním ho nedokázal předběhnout: boční sondy šikmo vpřed byly stále blíž k velkému než součet poloměrů, obě strany vyšly obsazené a rychlý čekal. Oprava: souseda, kterého nepřítel obchází, bere sonda v úvahu jen na jeho straně. Soused přesně vpředu neblokuje žádnou.</p>
<p>Velký nepřítel naopak odhazuje malé z cesty do stran, ve výseči ±45° před sebou. Neodhazujte ty, kteří jdou jeho směrem rychleji, než chce jít on, jinak odhodí i ty, kdo ho právě předbíhají.</p>
<h2 id="krok-8-sloty-u-cile-utoku"><a class="heading-anchor" href="#krok-8-sloty-u-cile-utoku" aria-hidden="true" tabindex="-1">#</a>Krok 8: Sloty u cíle útoku</h2>
<p>Pokud nepřátelé útočí zblízka, potřebují místo kolem cíle. Naše věž má 24 slotů po obvodu půdorysu 5 × 5 m, každý nepřítel zabírá jeden a ostatní čekají ve frontě.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/unity-dots-horde-navigation/tower-slots.webp" /><img src="https://smallwater.eu/blog/unity-dots-horde-navigation/tower-slots.png" alt="Věž s 24 sloty po obvodu, většina obsazená, žlutá dráha obchází věž obloukem k volnému slotu" width="1408" height="960" loading="lazy" decoding="async" /></picture><figcaption>Obsazené sloty útočí, volné jsou přerušované. Nový nepřítel obchází věž po oblouku k volnému slotu, za plnou stranou čeká fronta.</figcaption></figure>
<ul>
<li><strong>Obcházení po kruhu.</strong> Pokud nabízíte slot jen tehdy, když k němu vede přímá čára, zadní sloty zůstanou prázdné, protože zepředu čára k nim protíná věž. Ať nepřítel k volnému slotu obchází věž po oblouku.</li>
<li><strong>Bez vytlačování.</strong> Nevytlačujte nepřítele jdoucího ke slotu ve prospěch jiného žadatele, spouští to řetěz vytlačování.</li>
<li><strong>Bez odtlačování u slotu.</strong> Nepřítele přicházejícího vedle obsazeného slotu odtlačuje soused, takže skončí 13 cm od cíle nebo úplně mimo. Nepřátelé ve slotu a do 0,5 m od něj se navzájem neodtlačují.</li>
</ul>
<h2 id="vykon-simulace-a-vykreslovani"><a class="heading-anchor" href="#vykon-simulace-a-vykreslovani" aria-hidden="true" tabindex="-1">#</a>Výkon simulace a vykreslování</h2>
<p>Benchmark v Unity Editoru (M3 Pro, kolem 9 600 jednoduchých nepřátel, dvě úžiny) dal 90 FPS při 1440p a 100 při 1080p. Rozlišení FPS skoro nemění, zrychlení času ano: při 10× klesne na 20 FPS, protože na jeden snímek připadá deset kroků simulace. Při normální rychlosti má simulace rezervu.</p>
<p>S detailním modelem nepřítele místo kapsle se úzké hrdlo přesune do vykreslování:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Varianta</th>
<th align="right">Najednou</th>
<th align="right">Trojúhelníků na model</th>
<th align="right">Medián FPS</th>
</tr>
</thead>
<tbody><tr>
<td>Kapsle</td>
<td align="right">2 612</td>
<td align="right">844</td>
<td align="right">197</td>
</tr>
<tr>
<td>Detailní model, se stíny</td>
<td align="right">2 976</td>
<td align="right">3 578</td>
<td align="right">48</td>
</tr>
<tr>
<td>Detailní model, bez stínů</td>
<td align="right">2 976</td>
<td align="right">3 578</td>
<td align="right">85</td>
</tr>
<tr>
<td>Zjednodušený model, se stíny</td>
<td align="right">2 945</td>
<td align="right">878</td>
<td align="right">335</td>
</tr>
</tbody></table>
</div>
<p>V simulaci se vyplatí tři věci:</p>
<ul>
<li><strong>Dva průchody sousedů.</strong> Levný (dotyky, blokování) běží vždy, drahý (boční sondy, hustota stran) jen pro zablokované, což je ve volném proudu menšina. Vzdálenosti porovnávejte ve druhé mocnině, bez odmocniny.</li>
<li><strong>Jednoduché řazení.</strong> Synchronní seřazení spatial hashe trvalo při 3 000, 10 000, 20 000 a 50 000 záznamech 0,37, 1,49, 3,16 a 8,46 ms, verze v jobu 0,18, 0,93, 3,12 a 15,53 ms. Job není konzistentně rychlejší, jednoduché řazení stačí.</li>
<li><strong>Hash stavu.</strong> Při tiku 600 a 1800 spočítejte hash pozic a životů. Refaktor musí dát stejný hash, změna chování ho změní a umíte ji vysvětlit. Funguje to díky deterministickému pořadí ve spatial hashi.</li>
</ul>
<h2 id="limity-reseni"><a class="heading-anchor" href="#limity-reseni" aria-hidden="true" tabindex="-1">#</a>Limity řešení</h2>
<ul>
<li>Dva plné protisměrné davy v jedné úžině se zaseknou natrvalo. Je to limit modelu bez fyziky.</li>
<li>Čísla jsou z Unity Editoru na M3 Pro. Měření na Steam Decku a ve skutečném buildu zatím nemáme, jen odhad z veřejných benchmarků procesorů.</li>
</ul>
<p>Pokud chcete zkusit vlastní dav, <a href="https://docs.unity3d.com/Packages/com.unity.entities@6.5/manual/" target="_blank" rel="noopener">dokumentace Unity Entities</a> je dobrý začátek. Podobný postup od nejjednoduššího řešení k funkčnímu najdete i v článku o <a href="https://smallwater.eu/cs/blog/sudoku-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/unity-dots-horde-navigation/" data-ga-destination="/cs/blog/sudoku-generator/" data-ga-placement="inline">generátoru sudoku</a>. Jak nepřátele v takovém davu odhodit výbuchem bez drahého ragdollu pro každého, popisuje návod <a href="https://smallwater.eu/cs/blog/fake-ragdoll-unity-dots/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/unity-dots-horde-navigation/" data-ga-destination="/cs/blog/fake-ragdoll-unity-dots/" data-ga-placement="inline">jak udělat fake ragdoll v Unity DOTS</a>. Pokud řešíte podobnou simulaci v Unity a chcete pomoc, <a href="https://smallwater.eu/cs/#kontakt">napište nám</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Jak vygenerovat osmisměrku s tajenkou</title>
      <link>https://smallwater.eu/cs/blog/word-search-generator/</link>
      <guid isPermaLink="true">https://smallwater.eu/cs/blog/word-search-generator/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Křížovky</category>
      <description>Návod na generátor osmisměrky s tajenkou: příprava slovníku, losování délky slov, plnění mřížky bez backtrackingu a úspěšnost podle velikosti mřížky.</description>
      <content:encoded><![CDATA[<picture><source type="image/webp" srcset="https://smallwater.eu/blog/word-search-generator/cover.webp" /><img src="https://smallwater.eu/blog/word-search-generator/cover.jpg" alt="Osmisměrka 10×10 s barevně vyznačenými slovy a tajenkou v korálových políčkách" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Obsah"><p class="post-toc-title">Obsah</p><ol><li><a href="#pravidla-mrizky" data-ga="blog_toc_clicked" data-ga-heading-id="pravidla-mrizky">Pravidla mřížky</a></li><li><a href="#krok-1-pripravte-slovnik" data-ga="blog_toc_clicked" data-ga-heading-id="krok-1-pripravte-slovnik">Krok 1: Připravte slovník</a></li><li><a href="#krok-2-vyberte-tajenku" data-ga="blog_toc_clicked" data-ga-heading-id="krok-2-vyberte-tajenku">Krok 2: Vyberte tajenku</a></li><li><a href="#proc-nezacinat-tajenkou" data-ga="blog_toc_clicked" data-ga-heading-id="proc-nezacinat-tajenkou">Proč nezačínat tajenkou</a></li><li><a href="#krok-3-plnte-mrizku-dokud-nezustane-presne-n-der" data-ga="blog_toc_clicked" data-ga-heading-id="krok-3-plnte-mrizku-dokud-nezustane-presne-n-der">Krok 3: Plňte mřížku, dokud nezůstane přesně N děr</a></li><li><a href="#krok-4-losujte-delku-slova-s-rozumem" data-ga="blog_toc_clicked" data-ga-heading-id="krok-4-losujte-delku-slova-s-rozumem">Krok 4: Losujte délku slova s rozumem</a></li><li><a href="#krok-5-vlijte-tajenku-do-der" data-ga="blog_toc_clicked" data-ga-heading-id="krok-5-vlijte-tajenku-do-der">Krok 5: Vlijte tajenku do děr</a></li><li><a href="#kdyz-se-mrizka-nepovede" data-ga="blog_toc_clicked" data-ga-heading-id="kdyz-se-mrizka-nepovede">Když se mřížka nepovede</a></li><li><a href="#uspesnost-a-rychlost-podle-velikosti-mrizky" data-ga="blog_toc_clicked" data-ga-heading-id="uspesnost-a-rychlost-podle-velikosti-mrizky">Úspěšnost a rychlost podle velikosti mřížky</a></li><li><a href="#tip-stejna-mrizka-pri-kazdem-spusteni" data-ga="blog_toc_clicked" data-ga-heading-id="tip-stejna-mrizka-pri-kazdem-spusteni">Tip: stejná mřížka při každém spuštění</a></li></ol></nav><p>Osmisměrku s tajenkou vygenerujete tak, že do prázdné mřížky ukládáte náhodná slova ze slovníku, dokud v ní nezůstane volných přesně tolik políček, kolik písmen má tajenka. Ta pak vyplníte tajenkou zleva doprava a shora dolů. V telefonu to celé trvá pár milisekund a nepotřebuje backtracking, tedy vracení se k dřívějším rozhodnutím, když se generátor zasekne.</p>
<p>Osmisměrka je mřížka písmen, ve které hráč hledá a škrtá slova ze seznamu. Tajenka je text, který se přečte ze zbylých, nepřeškrtnutých písmen. Generátor proto musí složit mřížku, která je úplně plná a ve které po vyškrtání slov zůstane přesně tajenka. Postup níže je ten, kterým generujeme osmisměrky v naší aplikaci s křížovkami a osmisměrkami (<a href="https://smallwater.eu/crosswords-ios">App Store</a>, <a href="https://smallwater.eu/crosswords-android">Google Play</a>).</p>
<h2 id="pravidla-mrizky"><a class="heading-anchor" href="#pravidla-mrizky" aria-hidden="true" tabindex="-1">#</a>Pravidla mřížky</h2>
<p>Než začnete, ujasněte si, co má hotová mřížka splňovat. V naší aplikaci platí:</p>
<ul>
<li>Slova vedou v osmi směrech: vodorovně, svisle a po obou úhlopříčkách, každým směrem i pozpátku.</li>
<li>Dvě slova se mohou překrývat nejvýš v jednom písmenu.</li>
<li>Žádné slovo nesmí procházet políčkem tajenky.</li>
<li>Mřížka musí být plná. Prázdné políčko je chyba.</li>
<li>Tajenka se čte zleva doprava a shora dolů ze všech nepřeškrtnutých písmen.</li>
</ul>
<h2 id="krok-1-pripravte-slovnik"><a class="heading-anchor" href="#krok-1-pripravte-slovnik" aria-hidden="true" tabindex="-1">#</a>Krok 1: Připravte slovník</h2>
<p>Slovník rozdělte jednou na začátku do skupin podle délky slova. Výběr náhodného slova dané délky je pak jeden náhodný index. Kdybyste u každého slova filtrovali celý slovník, generování se znatelně zpomalí, protože do jedné mřížky se zkouší stovky slov.</p>
<p>Náš slovenský slovník má 16 713 slov s délkou od 3 do 21 písmen, nejvíc je sedmi- a osmipísmenných, český má 64 414 slov. Pár praktických rad:</p>
<ul>
<li><strong>Nechte ve slovníku i vlastní jména a zkratky.</strong> V mřížce se pak objeví třeba HOUSTON nebo MAINE. Bez krátkých a „divných“ slov nejde mřížku dostatečně zahustit.</li>
<li><strong>Vulgarismy filtrujte samostatným seznamem</strong>, který jde vypnout, pokud hráč chce obsah pro dospělé.</li>
<li><strong>U více jazyků načtěte jen ten, ve kterém se hraje.</strong> Anglický slovník má přes 414 000 slov. Načtení všech čtyř jazyků naráz při startu aplikace zmrazí slabší telefon až na 11 sekund.</li>
</ul>
<h2 id="krok-2-vyberte-tajenku"><a class="heading-anchor" href="#krok-2-vyberte-tajenku" aria-hidden="true" tabindex="-1">#</a>Krok 2: Vyberte tajenku</h2>
<p>Tajenka by měla mít aspoň tolik písmen, kolik je strana mřížky, a nejvýš o čtyři víc. Kratší v mřížce zanikne, delší vyžaduje příliš mnoho děr.</p>
<p>V naší aplikaci je tajenka pointa vtipu. Hráč škrtá slova a ze zbylých písmen si přečte, jak vtip končí. Tajenku skládáme z konce vtipu: odzadu přidáváme celá slova, dokud její délka nespadne do uvedeného rozsahu.</p>
<p>Obrázky níže používají anglický vtip „Two snakes walk into a bar. Turns out they were lizards.“ (Dva hadi vejdou do baru. Ukáže se, že to byly ještěrky.) na mřížce 10×10. Tajenka je WERELIZARDS, tedy 11 písmen. Slova v obrázcích jsou z anglického slovníku zúženého na běžná slova, aby se dala číst.</p>
<h2 id="proc-nezacinat-tajenkou"><a class="heading-anchor" href="#proc-nezacinat-tajenkou" aria-hidden="true" tabindex="-1">#</a>Proč nezačínat tajenkou</h2>
<p>První nápad je dělat to, co by člověk dělal tužkou: nejdřív rozházet písmena tajenky na náhodná políčka v pořadí čtení a potom kolem nich skládat slova.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/word-search-generator/v1-seed.webp" /><img src="https://smallwater.eu/blog/word-search-generator/v1-seed.png" alt="Prázdná mřížka 10×10 s jedenácti náhodně rozházenými písmeny tajenky" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Písmena tajenky rozházená do mřížky ještě před slovy.</figcaption></figure>
<p>Vypadá to rozumně, dokud nepřijde konec. Generátor si vybere prázdné políčko, zkusí přes něj dostat slovo ve všech směrech a posunech, a když mřížka zhoustne, zůstanou izolované díry. Jednu díru mezi dvěma písmeny tajenky nezaplní nic, protože nejkratší slovo má tři písmena a přes tajenku slovo vést nesmí.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/word-search-generator/v1-stuck.webp" /><img src="https://smallwater.eu/blog/word-search-generator/v1-stuck.png" alt="Téměř plná mřížka se třemi prázdnými políčky, která už nejdou zaplnit" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Po 10 000 pokusech zůstaly tři díry, které nevyplní žádné slovo.</figcaption></figure>
<p>Na vzorku 200 mřížek 10×10 tento přístup skončil s plnou mřížkou jen ve 28 % případů, v průměru zůstaly 1 až 2 díry. Opravovat je dodatečně je složité. Tajenku proto do mřížky nedávejte, dokud nejsou hotová slova.</p>
<h2 id="krok-3-plnte-mrizku-dokud-nezustane-presne-n-der"><a class="heading-anchor" href="#krok-3-plnte-mrizku-dokud-nezustane-presne-n-der" aria-hidden="true" tabindex="-1">#</a>Krok 3: Plňte mřížku, dokud nezůstane přesně N děr</h2>
<p>Plnění je jednoduchý greedy algorithm: každé slovo se položí hned, jak se vejde, a už se neodebírá. Generátor si pamatuje jen jedno číslo: kolik políček je ještě prázdných. Slovo položí jen tehdy, pokud by po něm nezůstalo méně prázdných políček, než má tajenka písmen (N).</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">prázdná = řádky × sloupce
dokud prázdná &gt; N a pokusů &lt; 2000:
    slovo = náhodné slovo zvolené délky
    opakuj nejvýš 50krát:
        začátek = náhodné políčko, směr = náhodný z 8
        pokud se slovo vejde podle pravidel mřížky:
            nová = počet prázdných políček, která slovo zaplní
            pokud prázdná - nová &gt;= N:
                polož slovo
                prázdná = prázdná - nová
                pokračuj dalším slovem
pokud prázdná != N: mřížku zahoď a začni znovu</code></pre>
<p>První slova padnou do prázdné mřížky volně:</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/word-search-generator/step-1.webp" /><img src="https://smallwater.eu/blog/word-search-generator/step-1.png" alt="Mřížka s prvními třemi slovy: jedno vodorovně a dvě svisle" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Po třech slovech. Každé slovo má vlastní barvu.</figcaption></figure>
<p>Postupně slova začínají sdílet písmena. To je vítané, protože každé sdílené písmeno je jedno políčko, které není třeba zaplňovat:</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/word-search-generator/step-2.webp" /><img src="https://smallwater.eu/blog/word-search-generator/step-2.png" alt="Mřížka se třinácti slovy, víc než třetina políček je prázdná" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Třináct slov, 40 prázdných políček.</figcaption></figure>
<p>Když počet prázdných políček klesne na N, plnění končí. Kde ty díry jsou, je jedno. Mohou být rozházené kdekoli, protože tajenka jimi nemusí vést žádnou cestou:</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/word-search-generator/step-3.webp" /><img src="https://smallwater.eu/blog/word-search-generator/step-3.png" alt="Mřížka s dvaadvaceti slovy a jedenácti prázdnými políčky s přerušovaným okrajem" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Zůstalo přesně 11 prázdných políček, stejně jako má WERELIZARDS písmen.</figcaption></figure>
<h2 id="krok-4-losujte-delku-slova-s-rozumem"><a class="heading-anchor" href="#krok-4-losujte-delku-slova-s-rozumem" aria-hidden="true" tabindex="-1">#</a>Krok 4: Losujte délku slova s rozumem</h2>
<p>Dlouhá slova dělají osmisměrku zajímavou, krátká ji dokážou dokončit. Délku proto losujte ze dvou normálních rozdělení (normálně rozdělená náhodná čísla dostanete třeba <a href="https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform" target="_blank" rel="noopener">Box-Mullerovou transformací</a>):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Pravděpodobnost</th>
<th>Délka slova</th>
</tr>
</thead>
<tbody><tr>
<td>50 %</td>
<td>kolem 6 písmen, směrodatná odchylka 2</td>
</tr>
<tr>
<td>30 %</td>
<td>kolem 5 písmen, směrodatná odchylka 1</td>
</tr>
<tr>
<td>20 %</td>
<td>3 písmena</td>
</tr>
</tbody></table>
</div>
<p>Délka je vždy aspoň 3 a nejvýš strana mřížky. K tomu dvě pravidla, bez kterých se generátor na konci zasekává:</p>
<ul>
<li><strong>Na konci vynuťte přesnou délku.</strong> Když do cíle chybí 5 a méně políček, hledejte slovo přesně takové délky, jaká chybí (nejméně 3). Jinak se bude generátor donekonečna pokoušet o šestipísmenná slova, která se už nevejdou.</li>
<li><strong>Třípísmenná slova dávejte na příděl.</strong> První tři pokusy o mřížku nesmí obsahovat ani jedno, pokusy 4 až 6 jedno, další tři dvě a tak dál. Třípísmenná slova jsou ve slovníku často zkratky typu DNA nebo CPU a v osmisměrce působí lacině. Povolte je, až když to jinak nejde.</li>
</ul>
<h2 id="krok-5-vlijte-tajenku-do-der"><a class="heading-anchor" href="#krok-5-vlijte-tajenku-do-der" aria-hidden="true" tabindex="-1">#</a>Krok 5: Vlijte tajenku do děr</h2>
<p>Projděte mřížku zleva doprava a shora dolů a do každého prázdného políčka zapište další písmeno tajenky. Protože děr je přesně N, tajenka se vejde beze zbytku.</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/word-search-generator/step-4.webp" /><img src="https://smallwater.eu/blog/word-search-generator/step-4.png" alt="Stejná mřížka, prázdná políčka vyplněná písmeny tajenky s pořadovými čísly 1 až 11" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Písmena tajenky v pořadí čtení. Hráč je najde jako poslední nepřeškrtnutá.</figcaption></figure>
<p>Hráč vidí jen tohle:</p>
<figure class="post-figure"><picture><source type="image/webp" srcset="https://smallwater.eu/blog/word-search-generator/final.webp" /><img src="https://smallwater.eu/blog/word-search-generator/final.png" alt="Hotová osmisměrka 10×10 bez vyznačení slov" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Hotová mřížka tak, jak ji vidí hráč.</figcaption></figure>
<aside class="app-box app-box-callout"><img class="app-box-img" src="https://smallwater.eu/images/crosswords/1-crosswords.jpg" alt="" width="698" height="1516" loading="lazy" decoding="async" /><div class="app-box-body"><p class="app-box-eyebrow">Vyzkoušejte</p><h2 class="app-box-title">Křížovky</h2><p>Křížovky, osmisměrky, sudoku a malované křížovky v jedné offline aplikaci. Jak hádanky vznikají a co je nového.</p><div class="app-box-links"><a class="btn btn-primary" href="https://smallwater.eu/crosswords-ios" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="crosswords" data-ga-destination="/crosswords-ios" data-ga-app="crosswords" data-ga-store="app_store">App Store</a><a class="btn btn-ghost" href="https://smallwater.eu/crosswords-android" data-ga="blog_app_link_clicked,outbound_store_click" data-ga-category="crosswords" data-ga-destination="/crosswords-android" data-ga-app="crosswords" data-ga-store="google_play">Google Play</a></div></div></aside>
<h2 id="kdyz-se-mrizka-nepovede"><a class="heading-anchor" href="#kdyz-se-mrizka-nepovede" aria-hidden="true" tabindex="-1">#</a>Když se mřížka nepovede</h2>
<p>Pokud generátor ani po 2 000 pokusech nedosáhne přesně N prázdných políček, celou mřížku zahoďte a začněte znovu, nejvýš 100krát. Žádné opravování děr. Je to hrubé, ale u mřížek do 12×12 to stačí s rezervou. Větší mřížky potřebují víc pokusů, jak ukazuje tabulka níže.</p>
<h2 id="uspesnost-a-rychlost-podle-velikosti-mrizky"><a class="heading-anchor" href="#uspesnost-a-rychlost-podle-velikosti-mrizky" aria-hidden="true" tabindex="-1">#</a>Úspěšnost a rychlost podle velikosti mřížky</h2>
<p>Generátor běžel na 300 mřížkách pro každou velikost, se skutečnými vtipy a slovenským slovníkem (Node.js 22 na serverovém procesoru, telefon bude pomalejší; obrázky v článku jsou z anglického slovníku, čísla ze slovenského, protože je menší, a tedy přísnější test). „Na první pokus“ znamená, že první kolo plnění skončilo plnou mřížkou.</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Mřížka</th>
<th align="right">Na první pokus</th>
<th align="right">Průměr pokusů</th>
<th align="right">Medián času</th>
<th align="right">Slov v mřížce</th>
<th align="right">Podíl úhlopříček</th>
</tr>
</thead>
<tbody><tr>
<td>8×8</td>
<td align="right">78 %</td>
<td align="right">1,30</td>
<td align="right">1,2 ms</td>
<td align="right">12,7</td>
<td align="right">24 %</td>
</tr>
<tr>
<td>10×10</td>
<td align="right">70 %</td>
<td align="right">1,42</td>
<td align="right">2,2 ms</td>
<td align="right">20,4</td>
<td align="right">27 %</td>
</tr>
<tr>
<td>12×12</td>
<td align="right">57 %</td>
<td align="right">1,73</td>
<td align="right">4,7 ms</td>
<td align="right">30,1</td>
<td align="right">29 %</td>
</tr>
<tr>
<td>15×15</td>
<td align="right">21 %</td>
<td align="right">5,46</td>
<td align="right">20 ms</td>
<td align="right">48,7</td>
<td align="right">33 %</td>
</tr>
</tbody></table>
</div>
<p>Co z toho plyne pro vlastní generátor:</p>
<ul>
<li><strong>Průměrné slovo má 4,7 písmene</strong>, i když cíl je kolem 6. Dlouhá slova se do zahuštěné mřížky prostě častěji nevejdou.</li>
<li><strong>Asi 9 až 10 % políček sdílejí dvě slova.</strong> Není třeba to nastavovat, plyne to z pravidla „nejvýš jedno společné písmeno“.</li>
<li><strong>Úhlopříček bude méně, než čekáte.</strong> Směr se losuje rovnoměrně z osmi, takže by měly tvořit polovinu slov. V malé mřížce ale úhlopříčné slovo mnohem častěji vypadne za okraj. Na 8×8 tvoří úhlopříčky 24 % slov, na 15×15 už 33 %. Pokud chcete víc úhlopříček, dejte jim při losování směru vyšší váhu.</li>
</ul>
<h2 id="tip-stejna-mrizka-pri-kazdem-spusteni"><a class="heading-anchor" href="#tip-stejna-mrizka-pri-kazdem-spusteni" aria-hidden="true" tabindex="-1">#</a>Tip: stejná mřížka při každém spuštění</h2>
<p>Na screenshoty do obchodů potřebujete, aby se vygenerovala vždy stejná mřížka. Místo běžného generátoru náhodných čísel použijte v takovém režimu jednoduchý lineární kongruenční generátor s pevným seedem (třeba odvozeným z jazyka). Pokud držíte veškerou aritmetiku v rozsahu, který čísla v JavaScriptu reprezentují přesně, výsledek je stejný na každém zařízení. Stejným trikem jsou vygenerované i obrázky v tomto článku.</p>
<p>Pokud řešíte podobný problém, generujete hlavolamy nebo potřebujete mobilní aplikaci, <a href="https://smallwater.eu/cs/#kontakt">napište nám</a>. Krátké ukázky z vývoje dáváme i na <a href="https://www.youtube.com/@smallwaterinc" target="_blank" rel="noopener">YouTube</a>. Podobně krok za krokem je popsáno i to, <a href="https://smallwater.eu/cs/blog/sudoku-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/word-search-generator/" data-ga-destination="/cs/blog/sudoku-generator/" data-ga-placement="inline">jak vygenerovat sudoku s jediným řešením</a>. Pokud potřebujete křížovku místo osmisměrky, podívejte se, <a href="https://smallwater.eu/cs/blog/swedish-crossword-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/cs/blog/word-search-generator/" data-ga-destination="/cs/blog/swedish-crossword-generator/" data-ga-placement="inline">jak vygenerovat švédskou křížovku s tajenkou</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
