<?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>Small Water Blog</title>
    <link>https://smallwater.eu/de/blog/</link>
    <description>Apps, Web-Tools und Spiele von einer kleinen Softwarefirma aus der Slowakei.</description>
    <language>de</language>
    <atom:link href="https://smallwater.eu/de/blog/feed.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Mon, 09 Nov 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>LOD für Tausende animierte Figuren in Unity erstellen</title>
      <link>https://smallwater.eu/de/blog/unity-lod-vertex-animation-crowd/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/unity-lod-vertex-animation-crowd/</guid>
      <pubDate>Mon, 09 Nov 2026 00:00:00 GMT</pubDate>
      <category>Tower Defense</category>
      <description>Anleitung für Crowd-LOD in Unity mit Vertex Animation Textures: warum Vertices statt Dreiecke zählen, LOD als neues Mesh und Auswahl pro Entity.</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="Drei Versionen derselben Skelettfigur mit von links nach rechts abnehmender Dreieckszahl" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#warum-generierter-lod-bei-einer-horde-nicht-hilft" data-ga="blog_toc_clicked" data-ga-heading-id="warum-generierter-lod-bei-einer-horde-nicht-hilft">Warum generierter LOD bei einer Horde nicht hilft</a></li><li><a href="#vertices-zahlen-nicht-dreiecke" data-ga="blog_toc_clicked" data-ga-heading-id="vertices-zahlen-nicht-dreiecke">Vertices zählen, nicht Dreiecke</a></li><li><a href="#schritt-1-vertices-in-lod-0-reduzieren" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-1-vertices-in-lod-0-reduzieren">Schritt 1: Vertices in LOD 0 reduzieren</a></li><li><a href="#schritt-2-lod-1-durch-vereinfachung-mit-gemeinsamer-textur" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-2-lod-1-durch-vereinfachung-mit-gemeinsamer-textur">Schritt 2: LOD 1 durch Vereinfachung mit gemeinsamer Textur</a></li><li><a href="#schritt-3-lod-2-als-neues-mesh-das-am-original-hangt" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-3-lod-2-als-neues-mesh-das-am-original-hangt">Schritt 3: LOD 2 als neues Mesh, das am Original hängt</a></li><li><a href="#schritt-4-locher-automatisch-prufen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-4-locher-automatisch-prufen">Schritt 4: Löcher automatisch prüfen</a></li><li><a href="#schritt-5-stufe-fur-jede-figur-einzeln-wahlen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-5-stufe-fur-jede-figur-einzeln-wahlen">Schritt 5: Stufe für jede Figur einzeln wählen</a></li><li><a href="#ergebnisse-und-grenzen" data-ga="blog_toc_clicked" data-ga-heading-id="ergebnisse-und-grenzen">Ergebnisse und Grenzen</a></li></ol></nav><p>Bauen Sie LOD für Tausende animierte Figuren in Unity nach der Zahl der Vertices, nicht der Dreiecke: Bei Vertex Animation Textures wird die Animation pro Vertex berechnet. Bereiten Sie die unteren Stufen von Hand vor, binden Sie die Vertices der niedrigsten Stufe an die Oberfläche des Originals und wählen Sie die Stufe für jede Figur einzeln. In unserem Tower-Defense-Spiel für den PC stieg die Bildrate bei 3 000 Gegnern in der Nahansicht der Horde so von 62 auf 120 FPS.</p>
<p>Das Verfahren betrifft Spiele, in denen sich Tausende Figuren gleichzeitig bewegen, also Tower Defense, Strategie und Survival. Unser Spiel entsteht in Unity mit ECS und ist in den <a href="https://smallwater.eu/de/#referencie">Referenzen</a> zu finden. LOD steht für <a href="https://en.wikipedia.org/wiki/Level_of_detail_(computer_graphics)" target="_blank" rel="noopener">Level of Detail</a>: Eine entfernte Figur wird aus einem einfacheren Mesh gezeichnet. Die Bilder zeigen die echten Meshes unseres Skelett-Gegners, keine Spielszenen.</p>
<h2 id="warum-generierter-lod-bei-einer-horde-nicht-hilft"><a class="heading-anchor" href="#warum-generierter-lod-bei-einer-horde-nicht-hilft" aria-hidden="true" tabindex="-1">#</a>Warum generierter LOD bei einer Horde nicht hilft</h2>
<p>Unity kann LOD-Stufen beim Import eines Modells erzeugen (Funktion <a href="https://docs.unity3d.com/6000.2/Documentation/Manual/lod/mesh-lod-introduction.html" target="_blank" rel="noopener">Mesh LOD</a>). Für Tausende Figuren hat das zwei Probleme.</p>
<p>Das erste ist das Aussehen. Die automatische Vereinfachung riss dünne Gliedmaßen auf und zerstörte die UVs, also die Zuordnung der Textur zum Mesh.</p>
<p>Das zweite sind die Kosten der Animation. Klassische Skelettanimation ist für Tausende Entities zu teuer, deshalb kommen Vertex Animation Textures (VAT) zum Einsatz. Eine VAT ist eine Textur, in der jede Spalte zu einem Vertex und jede Zeile zu einem Animationsframe gehört. Der Shader liest die Vertex-Position aus der Textur, die Kosten hängen also von der Zahl der gelesenen Vertices ab, auch beim Zeichnen der Schatten.</p>
<p>Die Obergrenze zeigt ein Test mit erzwungener generierter Stufe auf allen Figuren (Unity erzeugt auch die Stufen 3 und 4, Build, 1080p, 3 000 Gegner, Schatten an):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Erzwungene Stufe</th>
<th>Dreiecke</th>
<th>FPS</th>
</tr>
</thead>
<tbody><tr>
<td>LOD 0</td>
<td>11 Millionen</td>
<td>72</td>
</tr>
<tr>
<td>LOD 1</td>
<td>6 Millionen</td>
<td>96</td>
</tr>
<tr>
<td>LOD 2</td>
<td>3,68 Millionen</td>
<td>120</td>
</tr>
<tr>
<td>LOD 4</td>
<td>1,43 Millionen</td>
<td>201</td>
</tr>
</tbody></table>
</div>
<p>Der Spielraum ist groß. Er muss ohne sichtbare Löcher und ohne Sprünge zwischen den Stufen genutzt werden.</p>
<h2 id="vertices-zahlen-nicht-dreiecke"><a class="heading-anchor" href="#vertices-zahlen-nicht-dreiecke" aria-hidden="true" tabindex="-1">#</a>Vertices zählen, nicht Dreiecke</h2>
<p>Halb so viele Dreiecke helfen für sich genommen wenig. Ein LOD mit halb so vielen Dreiecken und 996 von ursprünglich 1 521 Vertices sparte 1,9 ms pro Frame, ein LOD mit 217 Vertices 6,4 ms. Beobachten Sie bei jeder Stufe beide Werte.</p>
<p>Oft gibt es unnötig viele Vertices. An jeder UV-Naht wird ein Vertex verdoppelt, weil eine Position dann zwei Texturkoordinaten braucht. Unsere Modelle aus einem KI-Generator hatten UVs in Hunderten Inseln: Das Skelett hatte 1 521 Vertices, aber nur 608 verschiedene Kombinationen aus Position und Normale.</p>
<h2 id="schritt-1-vertices-in-lod-0-reduzieren"><a class="heading-anchor" href="#schritt-1-vertices-in-lod-0-reduzieren" aria-hidden="true" tabindex="-1">#</a>Schritt 1: Vertices in LOD 0 reduzieren</h2>
<p>Die erste Einsparung kostet kein Detail. Verschmelzen Sie Vertices mit gleicher Position und ähnlicher Normale (Abweichung bis 25°), entfalten Sie die UVs neu mit Nähten nur an scharfen Kanten (über 70°) und übertragen Sie die Textur Dreieck für Dreieck aus der alten. Beim Skelett sank die Zahl von 1 521 auf 1 090 Vertices, bei den übrigen Gegnertypen um 19 bis 29 %, bei gleicher Form.</p>
<p>Schreiben Sie danach nur die Spalten in die VAT, die eine Stufe tatsächlich nutzt. Das sparte 40 bis 60 % des Animationsspeichers.</p>
<h2 id="schritt-2-lod-1-durch-vereinfachung-mit-gemeinsamer-textur"><a class="heading-anchor" href="#schritt-2-lod-1-durch-vereinfachung-mit-gemeinsamer-textur" aria-hidden="true" tabindex="-1">#</a>Schritt 2: LOD 1 durch Vereinfachung mit gemeinsamer Textur</h2>
<p>LOD 1 ist das auf etwa 52 % der Dreiecke vereinfachte LOD 0. Vereinfachen Sie stückweise (jede Kombination aus Bone und zusammenhängendem Teil des Meshes) und sperren Sie die Vertices an den Rändern der Stücke. Geben Sie dem Kopf ein größeres Budget, denn seine Silhouette erkennt das Auge in der Menge.</p>
<p>Behalten Sie die Textur von LOD 0. Eine Stufe mit eigener Textur oder anderer Form ist beim Übergang auch bei ähnlicher Silhouette sichtbar.</p>
<p>Unter etwa 50 % der Dreiecke reißt das Vereinfachen aus vorhandenen Vertices dünne Teile und einzelne Stücke auf. Noch weiter nach unten braucht es einen anderen Weg.</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="Drei Versionen der Skelettfigur nebeneinander mit abnehmender Dreieckszahl" width="2400" height="1240" loading="lazy" decoding="async" /></picture><figcaption>Dasselbe Skelett in drei Stufen: 873, 448 und 330 Dreiecke, 1 090, 705 und 520 Vertices.</figcaption></figure>
<h2 id="schritt-3-lod-2-als-neues-mesh-das-am-original-hangt"><a class="heading-anchor" href="#schritt-3-lod-2-als-neues-mesh-das-am-original-hangt" aria-hidden="true" tabindex="-1">#</a>Schritt 3: LOD 2 als neues Mesh, das am Original hängt</h2>
<p>Die niedrigste Stufe ist ein neues geschlossenes Mesh. Verdicken Sie das Original mit einer Schicht, lassen Sie ein Voxel-Remesh darüberlaufen und vereinfachen Sie auf die Zielzahl (bei uns 200 bis 460 Dreiecke je nach Gegnertyp). Ziehen Sie das Ergebnis auf die Oberfläche des Originals und erzeugen Sie die UVs automatisch. Backen Sie die Textur aus dem Original in derselben Dichte wie LOD 0, damit auch die Mipmaps passen.</p>
<p>Das neue Mesh hat keine Animationsgewichte. Binden Sie deshalb jeden seiner Vertices an das nächstgelegene Dreieck des Originals und speichern Sie die drei Spalten dieses Dreiecks und die baryzentrischen Gewichte (die Anteile, die die Position eines Punkts im Dreieck festlegen).</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="Schema: Ein roter Vertex des vereinfachten Meshes ist gestrichelt mit den drei Vertices eines Originaldreiecks verbunden" width="1800" height="840" loading="lazy" decoding="async" /></picture><figcaption>Ein Vertex der unteren Stufe (rot) bewegt sich mit den drei Vertices des nächsten Originaldreiecks.</figcaption></figure>
<p>Die VAT-Spalte eines angehängten Vertex ist in jedem Frame die gewichtete Summe der Positionen und Normalen dieser drei Vertices. Der Shader muss nichts Besonderes tun, ein angehängter Vertex ist einfach eine weitere Spalte.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">für jeden Frame f:
    VAT[angehängt, f] = w0 * VAT[a, f] + w1 * VAT[b, f] + w2 * VAT[c, f]</code></pre>
<h2 id="schritt-4-locher-automatisch-prufen"><a class="heading-anchor" href="#schritt-4-locher-automatisch-prufen" aria-hidden="true" tabindex="-1">#</a>Schritt 4: Löcher automatisch prüfen</h2>
<p>Bilder aus dem Modellierungswerkzeug zeigen Löcher nicht zuverlässig. Eine einfache Prüfung genügt: Rendern Sie Original und Kandidat aus 11 Richtungen mit 120 px und vergleichen Sie die Silhouetten.</p>
<p>Als Loch gelten Pixel des Originals innerhalb der Kontur des Kandidaten, die der Kandidat nicht abdeckt, sowie Pixel des Kandidaten mehr als 2 px außerhalb der Originalkontur. Bei uns gelten als Grenzen 1 % Löcher und 4 bis 6 % Verlust. Die Prüfung muss das materialabhängige Backface-Culling beachten. Die Grenzen haben wir danach festgelegt, wann Übergänge im Spiel sichtbar waren.</p>
<h2 id="schritt-5-stufe-fur-jede-figur-einzeln-wahlen"><a class="heading-anchor" href="#schritt-5-stufe-fur-jede-figur-einzeln-wahlen" aria-hidden="true" tabindex="-1">#</a>Schritt 5: Stufe für jede Figur einzeln wählen</h2>
<p>Wenn die ganze Karte gleichzeitig die Stufe wechselt, sieht man das. Bei einer Auswahl pro Entity verteilen sich die Übergänge und gehen in der Menge unter.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Ganze Karte auf einmal</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="Menge von Punkten in einer Farbe, alle Figuren haben dieselbe Stufe" width="1120" height="840" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Pro Entity</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="Menge von Punkten in Bändern nach Abstand zur Kamera, nahe rot, ferne grau" width="1120" height="840" loading="lazy" decoding="async" /></picture></div></div><figcaption>Blick von oben, Kamera unten. Farbe ist die LOD-Stufe: Rot 0, Orange 1, Grau 2.</figcaption></figure>
<p>Hinweis zur Version: In der Dokumentation von Unity 6.2 steht, dass Entities Graphics immer LOD 0 verwendet. Die Auswahl pro Entity mit der folgenden Formel nutzen wir in Unity 6.6, prüfen Sie daher vorab das Verhalten Ihrer Version:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">LOD = log2(d * C / D) * steigung + versatz
C = schwelle * 2 * tan(fov / 2) / bildhoehe</code></pre>
<p>Dabei ist <code>d</code> der Abstand zur Kamera und <code>D</code> der Durchmesser der World Bounds der Entity. Wählen Sie Steigung und Versatz so, dass Stufe 1 bei 93,3 und Stufe 2 bei 167,9 Metern beginnt (Kamera mit 30° FOV und 1440 px). Setzen Sie die globale LOD-Schwelle auf Bildhöhe / 1440, dann hängen die Abstände nicht von der Auflösung ab.</p>
<p>Zwei Fehler, die Sie vermeiden sollten:</p>
<ul>
<li><strong>Skalierung.</strong> Berechnen Sie den Durchmesser <code>D</code> aus den World Bounds, also mit der Skalierung des Prefabs. Meshes für VAT liegen oft in kleinen Einheiten vor, das Prefab vergrößert sie 2- bis 4-fach. Ohne das zeichnet das Spiel die ganze Zeit LOD 0 und meldet nichts. Prüfen Sie das anhand der Dreieckszahl im Normalbetrieb gegenüber einem erzwungenen LOD. Die Drehung einer Entity vergrößert die achsenparallelen Bounds, deshalb multiplizieren wir den Durchmesser bei uns mit 1,22.</li>
<li><strong>Baking.</strong> Die Auswahlkurve wird beim Baken der SubScene gespeichert, und das Baking erkennt eine Änderung am Mesh nicht. Erzwingen Sie nach der Anpassung einen Neuaufbau der SubScene.</li>
</ul>
<h2 id="ergebnisse-und-grenzen"><a class="heading-anchor" href="#ergebnisse-und-grenzen" aria-hidden="true" tabindex="-1">#</a>Ergebnisse und Grenzen</h2>
<p>macOS-Build, 2560 × 1440, 3 000 Gegner, Schatten an, Spiel nach dem Game Over angehalten (VAT-Animationen laufen weiter, die Messung gilt also für die Grafik):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Szene</th>
<th>Vorher</th>
<th>Nachher</th>
</tr>
</thead>
<tbody><tr>
<td>Ganze Karte</td>
<td>90 FPS</td>
<td>130 FPS</td>
</tr>
<tr>
<td>Nahansicht der Horde</td>
<td>62 FPS</td>
<td>120 FPS</td>
</tr>
</tbody></table>
</div>
<p>Auf dem <a href="https://www.steamdeck.com/en/tech" target="_blank" rel="noopener">Steam Deck</a> (1280 × 800, 8 Recheneinheiten RDNA 2) sind es 75 bis 80 FPS auf der ganzen Karte. Die engste Nahansicht der Horde läuft mit hoher Schattenqualität bei 33 FPS, ohne Schatten bei 64. Schatten sind damit der größte verbleibende Posten: LOD senkt auch ihre Kosten, aber das Abschalten bringt immer noch einen großen Sprung.</p>
<p>Was LOD nicht löst: Die Lochprüfung läuft in der T-Pose und erkennt daher kein Dehnen in extremen Posen. Ein Gegnertyp überschreitet die Lochgrenze in LOD 2 noch. Die Berechnung der Hordenbewegung ändert LOD nicht, dazu gibt es den Artikel zur <a href="https://smallwater.eu/de/blog/unity-dots-horde-navigation/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/unity-lod-vertex-animation-crowd/" data-ga-destination="/de/blog/unity-dots-horde-navigation/" data-ga-placement="inline">Hordennavigation in Unity DOTS</a>, zu den Reaktionen auf Explosionen den Artikel zum <a href="https://smallwater.eu/de/blog/fake-ragdoll-unity-dots/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/unity-lod-vertex-animation-crowd/" data-ga-destination="/de/blog/fake-ragdoll-unity-dots/" data-ga-placement="inline">Fake Ragdoll</a>.</p>
<p>Brauchen Sie Hilfe bei der Performance Ihres Unity-Spiels oder bei einer Crowd-Simulation? <a href="https://smallwater.eu/de/#kontakt">Schreiben Sie uns</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Glow und 2D-Lichter für alte 2D-Spiele in Unity URP</title>
      <link>https://smallwater.eu/de/blog/dreamica-remaster-unity-urp-glow/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/dreamica-remaster-unity-urp-glow/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Dreamica</category>
      <description>So bekommt ein altes 2D-Spiel in Unity URP selektiven Glow mit Bloom und HDR, dynamische 2D-Lichter und eine scharfe UI. Am Beispiel des Spiels 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="Drei Szenen aus Dreamica: ein Geist jagt eine Blase, ein Eichhörnchen mit Kanone und leuchtende Laser im Wald" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#warum-der-alte-vollbild-glow-nicht-reicht" data-ga="blog_toc_clicked" data-ga-heading-id="warum-der-alte-vollbild-glow-nicht-reicht">Warum der alte Vollbild-Glow nicht reicht</a></li><li><a href="#schritt-1-projekt-auf-urp-umstellen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-1-projekt-auf-urp-umstellen">Schritt 1: Projekt auf URP umstellen</a><ol><li><a href="#graues-bild-camera-stack-einrichten" data-ga="blog_toc_clicked" data-ga-heading-id="graues-bild-camera-stack-einrichten">Graues Bild: Camera Stack einrichten</a></li><li><a href="#sprites-ignorieren-licht-auf-den-2d-renderer-wechseln" data-ga="blog_toc_clicked" data-ga-heading-id="sprites-ignorieren-licht-auf-den-2d-renderer-wechseln">Sprites ignorieren Licht: auf den 2D Renderer wechseln</a></li><li><a href="#partikel-und-alte-plugins" data-ga="blog_toc_clicked" data-ga-heading-id="partikel-und-alte-plugins">Partikel und alte Plugins</a></li></ol></li><li><a href="#schritt-2-selektiver-glow-mit-hdr-farbe-und-bloom-schwelle-1" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-2-selektiver-glow-mit-hdr-farbe-und-bloom-schwelle-1">Schritt 2: Selektiver Glow mit HDR-Farbe und Bloom-Schwelle 1</a><ol><li><a href="#emission-map-nur-ein-teil-des-sprites-leuchtet" data-ga="blog_toc_clicked" data-ga-heading-id="emission-map-nur-ein-teil-des-sprites-leuchtet">Emission Map: nur ein Teil des Sprites leuchtet</a></li><li><a href="#pulsieren-in-belichtungsstufen" data-ga="blog_toc_clicked" data-ga-heading-id="pulsieren-in-belichtungsstufen">Pulsieren in Belichtungsstufen</a></li></ol></li><li><a href="#schritt-3-dynamisches-2d-licht-um-den-spieler" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-3-dynamisches-2d-licht-um-den-spieler">Schritt 3: Dynamisches 2D-Licht um den Spieler</a><ol><li><a href="#derselbe-shader-auf-angrenzenden-flachen" data-ga="blog_toc_clicked" data-ga-heading-id="derselbe-shader-auf-angrenzenden-flachen">Derselbe Shader auf angrenzenden Flächen</a></li><li><a href="#lichtblitz-zum-hohepunkt-uberblenden-statt-addieren" data-ga="blog_toc_clicked" data-ga-heading-id="lichtblitz-zum-hohepunkt-uberblenden-statt-addieren">Lichtblitz: zum Höhepunkt überblenden statt addieren</a></li></ol></li><li><a href="#schritt-4-scharfe-ui-fur-heutige-displays" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-4-scharfe-ui-fur-heutige-displays">Schritt 4: Scharfe UI für heutige Displays</a></li><li><a href="#grenzen-unkomprimierte-texturen" data-ga="blog_toc_clicked" data-ga-heading-id="grenzen-unkomprimierte-texturen">Grenzen: unkomprimierte Texturen</a></li></ol></nav><p>Ein altes 2D-Spiel in Unity auf der Built-in Render Pipeline wirkt auf einem heutigen Smartphone flach: Es gibt kein Licht, der Glow fehlt oder lässt das ganze Bild samt UI leuchten, und die Sprites der Oberfläche sind unscharf. In Unity URP lösen Sie das mit drei Dingen: Bloom mit Schwelle 1 und HDR-Farbe nur auf Objekten, die leuchten sollen, 2D-Lichtern aus dem 2D Renderer und einer UI mit größeren Sprites, die den sicheren Bereich des Displays beachtet.</p>
<p>Wir zeigen das Vorgehen an unserem Spiel Dreamica (<a href="https://smallwater.eu/dreamica-ios">App Store</a>, <a href="https://smallwater.eu/dreamica-android">Google Play</a>). Es ist ein Fall-Abenteuer, in dem Sie mit dem Finger Linien zeichnen und damit eine Blase mit einem schlafenden Kind durch vier Welten lenken. Ursprünglich entstand es in Unity 5.3, die UI lief im Plugin NGUI, den Glow lieferte das Plugin MK Glow und die Grafik war für das iPhone 7 mit seinem Display von 1334 × 750 Pixeln gemacht. Das Gameplay blieb gleich, geändert hat sich nur die Grafik.</p>
<p>Ein altes Spiel öffnet man oft wegen des Stores, nicht wegen der Grafik. Google Play <a href="https://android-developers.googleblog.com/2019/01/get-your-apps-ready-for-64-bit.html" target="_blank" rel="noopener">verlangt seit August 2019 64-Bit-Versionen</a>, und ein Spiel mit nur einem 32-Bit-Build fliegt aus dem Store. Genau so erging es Dreamica. Wenn Sie das Projekt ohnehin nach Unity 6 bringen, ist die Grafik ein relativ günstiger Bonus.</p>
<h2 id="warum-der-alte-vollbild-glow-nicht-reicht"><a class="heading-anchor" href="#warum-der-alte-vollbild-glow-nicht-reicht" aria-hidden="true" tabindex="-1">#</a>Warum der alte Vollbild-Glow nicht reicht</h2>
<p>Ältere Glow-Plugins arbeiten als Vollbild-Effekt. Sie lassen alles Helle leuchten, auch die Icon-Buttons im Menü. In Dreamica verschwanden die Icons unter MK Glow in Unity 6 fast. Das offizielle Post Processing Stack v2 ist nur ein Ersatz für die Built-in Pipeline und greift in URP nicht mehr. Wenn Sie einen Glow wollen, den Sie pro Material steuern, und dazu dynamische 2D-Lichter, wechseln Sie zu URP und nutzen Sie das dort eingebaute Bloom.</p>
<h2 id="schritt-1-projekt-auf-urp-umstellen"><a class="heading-anchor" href="#schritt-1-projekt-auf-urp-umstellen" aria-hidden="true" tabindex="-1">#</a>Schritt 1: Projekt auf URP umstellen</h2>
<p>Nach dem Wechsel zu URP stößt ein altes 2D-Spiel fast sicher auf zwei Fallen. Beide zeigen sich so, dass das Bild falsch oder genau wie vorher aussieht, obwohl die Shader in Ordnung sind.</p>
<h3 id="graues-bild-camera-stack-einrichten"><a class="heading-anchor" href="#graues-bild-camera-stack-einrichten" aria-hidden="true" tabindex="-1">#</a>Graues Bild: Camera Stack einrichten</h3>
<p>Zeigt die Game-Ansicht im Editor nur eine graue Fläche, während die Scene-Ansicht und ein einmaliger Kamera-Render richtig aussehen, liegt der Fehler nicht in den Shadern, sondern im Zusammensetzen der Kameras. Alte Spiele haben oft zwei Kameras, eine für die Welt und eine für die UI, und die UI-Kamera löscht nur die Tiefe. So werden Kameras in der Built-in Pipeline geschichtet, und URP unterstützt das nicht. Es braucht einen expliziten Camera Stack: Eine Kamera ist die Basis (Base), die anderen sind Überlagerungen (Overlay) in ihrem Stack. Richten Sie ihn in jeder Szene ein, in der Kameras übereinanderliegen.</p>
<h3 id="sprites-ignorieren-licht-auf-den-2d-renderer-wechseln"><a class="heading-anchor" href="#sprites-ignorieren-licht-auf-den-2d-renderer-wechseln" aria-hidden="true" tabindex="-1">#</a>Sprites ignorieren Licht: auf den 2D Renderer wechseln</h3>
<p>Sprites mit beleuchtetem Shader reagieren auf kein Licht, wenn der normale URP-Renderer aktiv ist. Er berechnet 2D-Lichter überhaupt nicht. Wechseln Sie auf den 2D Renderer. Auch dieser zeichnet keine Beleuchtung, solange die Szene kein einziges 2D-Licht enthält. Wenn etwas nach einer Änderung genau wie vorher aussieht, prüfen Sie zuerst, welcher Renderer läuft.</p>
<h3 id="partikel-und-alte-plugins"><a class="heading-anchor" href="#partikel-und-alte-plugins" aria-hidden="true" tabindex="-1">#</a>Partikel und alte Plugins</h3>
<p>Partikel mit alten Shadern werden im 2D Renderer schwarz oder gar nicht gezeichnet. Geben Sie ihnen einen neuen, einfachen Shader.</p>
<p>Vorsicht auch beim Asset Store. Beim Aktualisieren der Assets kann der Editor von sich aus ein „Update“ eines alten Plugins anwenden, das Metadaten hinzufügt und Shader umbenennt. Materialien finden ihren Shader über den Namen und gehen dadurch unbemerkt kaputt. Sehen Sie sich nach jeder größeren Asset-Aktualisierung in einem alten Projekt alle geänderten Dateien in der Versionsverwaltung an, auch die, an denen Sie gerade nicht arbeiten.</p>
<h2 id="schritt-2-selektiver-glow-mit-hdr-farbe-und-bloom-schwelle-1"><a class="heading-anchor" href="#schritt-2-selektiver-glow-mit-hdr-farbe-und-bloom-schwelle-1" aria-hidden="true" tabindex="-1">#</a>Schritt 2: Selektiver Glow mit HDR-Farbe und Bloom-Schwelle 1</h2>
<p>Ziel ist, dass ausgewählte Objekte leuchten, in Dreamica die gezeichnete Linie, Laser, Funken, Glühwürmchen und Runen, aber nicht Hintergründe und UI. Bloom in URP verwischt und addiert alles, was heller ist als seine <a href="https://docs.unity3d.com/6/Documentation/Manual/urp/post-processing-bloom.html" target="_blank" rel="noopener">Schwelle</a>. Setzen Sie die Schwelle auf 1 und heben Sie die Farbe der Materialien, die leuchten sollen, über 1.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">Linien-Shader:
    Farbe = Textur × Vertexfarbe × Tönung × Intensität    (Intensität = 6)

Bloom:
    Schwelle = 1, Stärke = 0,3
    es leuchtet nur, was nach der Berechnung heller als 1 ist</code></pre>
<p>Hintergründe und Buttons haben Farben zwischen 0 und 1, kommen nicht über die Schwelle und bleiben scharf. Damit verschwinden auch die ausgebleichten Icons. Voraussetzung ist eine Kamera mit HDR. Ohne HDR wird die Farbe auf 1 begrenzt und nichts leuchtet. Der 2D Renderer zeigt den HDR-Schalter im Kamera-Inspector nicht an, schalten Sie HDR deshalb per Skript ein.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Niedrige Schwelle, ganzes Bild</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="Vollbild-Glow: Linie, Button-Rand und helle Teile des Hintergrunds leuchten" width="1440" height="840" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Schwelle 1, HDR-Material</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="Selektiver Glow: Nur die gezeichnete Linie leuchtet, Button und Hintergrund bleiben scharf" width="1440" height="840" loading="lazy" decoding="async" /></picture></div></div><figcaption>Illustration des Prinzips. Links Vollbild-Glow mit niedriger Schwelle, rechts leuchtet nur das Material mit Intensität über 1.</figcaption></figure>
<p>Wenn der Glow so unter Kontrolle ist, muss er nicht optional sein. In Dreamica haben wir den Glow-Schalter aus den Einstellungen entfernt, der Glow gehört zum Look.</p>
<h3 id="emission-map-nur-ein-teil-des-sprites-leuchtet"><a class="heading-anchor" href="#emission-map-nur-ein-teil-des-sprites-leuchtet" aria-hidden="true" tabindex="-1">#</a>Emission Map: nur ein Teil des Sprites leuchtet</h3>
<p>Soll nur ein Teil eines Sprites leuchten, geben Sie ihm eine Emission Map: eine zweite, schwarz-weiße Textur, die festlegt, welcher Teil des Sprites leuchtet. Nur dieser Teil bekommt eine Farbe über 1, sodass bei einer Rune der Kern strahlt und nicht der ganze Umriss.</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="Eine Rune, ihre schwarz-weiße Emission Map und das Ergebnis mit leuchtendem Kern" width="1800" height="600" loading="lazy" decoding="async" /></picture><figcaption>Sprite der Rune, Emission Map (Weiß leuchtet) und das Ergebnis nach dem Bloom.</figcaption></figure>
<h3 id="pulsieren-in-belichtungsstufen"><a class="heading-anchor" href="#pulsieren-in-belichtungsstufen" aria-hidden="true" tabindex="-1">#</a>Pulsieren in Belichtungsstufen</h3>
<p>Soll der Glow pulsieren, multiplizieren Sie die Farbe nicht linear. Ein HDR-Material kann eine Helligkeit um 2 hoch 8 haben, und ein linearer Faktor von 0 bis 10 ist darauf überhaupt nicht zu sehen. Rechnen Sie den Puls in Belichtungsstufen (EV), genau wie der Intensitätsregler in Unitys HDR-Farbwähler:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">Farbe = Tönung × 2 ^ ev,   ev schwingt zwischen einem Minimum und einem Maximum</code></pre>
<h2 id="schritt-3-dynamisches-2d-licht-um-den-spieler"><a class="heading-anchor" href="#schritt-3-dynamisches-2d-licht-um-den-spieler" aria-hidden="true" tabindex="-1">#</a>Schritt 3: Dynamisches 2D-Licht um den Spieler</h2>
<p>Setzen Sie ein 2D-Punktlicht auf den Spieler und um ihn einen weichen Schein. In Dreamica steuert eine einzige Sinuskurve gleichzeitig die Helligkeit des Scheins, den Radius des Lichts und seine Intensität, sodass alles gemeinsam pulsiert. Die Lichtfarbe kann die Umgebung bestimmen: im Wald weiß, im Sumpf orange, im Winter violett, in der Hölle gelb. Stellen Sie Hintergründe und Hindernisse auf beleuchtete Shader um, sonst reagieren sie nicht auf das Licht.</p>
<p>Kurze Lichter beleben Ereignisse. Wenn das Eichhörnchen mit der Kanone schießt, blitzt ein bernsteinfarbenes Licht mit Intensität 3 auf und erlischt in 0,3 Sekunden, und die Funken aus dem Rohr leuchten. Nicht alles muss leuchten: Der Staub beim Verschwinden der Eichel ist absichtlich braun und leuchtet nicht.</p>
<h3 id="derselbe-shader-auf-angrenzenden-flachen"><a class="heading-anchor" href="#derselbe-shader-auf-angrenzenden-flachen" aria-hidden="true" tabindex="-1">#</a>Derselbe Shader auf angrenzenden Flächen</h3>
<p>Wenn zwei Hintergründe aneinandergrenzen, die Quellbilder gleich hell sind und trotzdem eine sichtbare Naht bleibt, prüfen Sie die Shader. In Dreamica hatten der Starthintergrund und der endlose Waldhintergrund jeweils einen anderen Shader. Einer ignorierte die Materialtönung von 0,733, der andere wandte sie an und dunkelte den Wald auf 73 % ab. Die Lösung ist derselbe Shader auf beiden Flächen.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Zwei Shader</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="Grenze zwischen zwei Waldhintergründen, der untere Teil ist deutlich dunkler" width="1024" height="1120" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Ein 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="Dieselbe Grenze, beide Hintergründe haben dieselbe Helligkeit" width="1024" height="1120" loading="lazy" decoding="async" /></picture></div></div><figcaption>Grenze zwischen Starthintergrund und Wald. Links ist der untere Teil durch die Tönung 0,733 abgedunkelt, die der obere Shader ignoriert.</figcaption></figure>
<h3 id="lichtblitz-zum-hohepunkt-uberblenden-statt-addieren"><a class="heading-anchor" href="#lichtblitz-zum-hohepunkt-uberblenden-statt-addieren" aria-hidden="true" tabindex="-1">#</a>Lichtblitz: zum Höhepunkt überblenden statt addieren</h3>
<p>Beim Übergang in die nächste Welt flammen Licht und Schein auf: 0,6 Sekunden Anstieg, kurzes Halten, 1,4 Sekunden Abklingen, und der Schein dreht sich dabei einmal. Wenn Sie den Blitz einfach zum Puls addieren, hängt der Höhepunkt davon ab, wo die Sinuskurve gerade steht, und jeder Blitz ist unterschiedlich stark. Blenden Sie stattdessen fließend vom Puls zu einem festen Höhepunkt über:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">Gewicht = Blitzkurve (0 bis 1)
Wert = Puls + (Höhepunkt - Puls) × Gewicht</code></pre>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Addiert</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="Intensitätsdiagramm: Puls und zwei Blitze mit unterschiedlich hohem Höhepunkt" width="1440" height="720" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Überblendet</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="Intensitätsdiagramm: Puls und zwei Blitze mit gleichem Höhepunkt auf der gepunkteten Linie" width="1440" height="720" loading="lazy" decoding="async" /></picture></div></div><figcaption>Zwei Blitze in unterschiedlicher Phase des Pulses (orange und blau). Links unterscheiden sich die Höhepunkte, rechts erreichen beide denselben Wert.</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">Ausprobieren</p><h2 class="app-box-title">Dreamica</h2><p>Ein handgezeichnetes Fall-Abenteuer, in dem du physikalische Linien zeichnest, um eine magische Blase zu lenken. Entwicklungsnotizen und Neuigkeiten.</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="schritt-4-scharfe-ui-fur-heutige-displays"><a class="heading-anchor" href="#schritt-4-scharfe-ui-fur-heutige-displays" aria-hidden="true" tabindex="-1">#</a>Schritt 4: Scharfe UI für heutige Displays</h2>
<p>Eine UI, die für das iPhone 7 gezeichnet wurde, hat kleine Sprites. Der Menü-Button in Dreamica hatte ein Sprite mit 128 × 64 Pixeln und wurde auf einem Smartphone mit dreifacher Pixeldichte um ein Vielfaches gestreckt. Zeichnen Sie die Sprites in höherer Auflösung neu: Der neue Button hat 800 × 434, die Fortschrittsbalken wuchsen von 200 × 26 auf 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="Nahaufnahme eines unscharfen Button-Rands" 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="Nahaufnahme eines scharfen Button-Rands mit sichtbarer Textur" width="1120" height="608" loading="lazy" decoding="async" /></picture></div></div><figcaption>Derselbe Button in derselben Größe auf dem Display. Links das ursprüngliche Sprite, rechts das neue.</figcaption></figure>
<p>Wenn die UI auf NGUI basiert, ziehen Sie eine Neuentwicklung in Betracht. Die Anker in NGUI werden stückweise über mehrere Frames berechnet, lange Ankerketten pendeln sich manchmal falsch ein, und der Maßstab unterscheidet sich zwischen Editor, Smartphone und iPad. In Dreamica haben Korrekturen das nicht zuverlässig gelöst, deshalb haben wir die UI in uGUI und TextMeshPro neu geschrieben. Gehen Sie Bildschirm für Bildschirm vor und lassen Sie die alte Version daneben als Vorlage laufen. Skalieren Sie die UI nach der Höhe gegenüber einer Referenzauflösung (bei uns 434 × 941 Punkte) und beachten Sie den sicheren Bereich des Displays, also Notch und abgerundete Ecken. Entfernen Sie NGUI danach vollständig aus dem Projekt, damit nicht zwei UI-Systeme übrig bleiben.</p>
<h2 id="grenzen-unkomprimierte-texturen"><a class="heading-anchor" href="#grenzen-unkomprimierte-texturen" aria-hidden="true" tabindex="-1">#</a>Grenzen: unkomprimierte Texturen</h2>
<p>Dieses Vorgehen ändert nur die Grafik. In Dreamica haben wir die Texturen bewusst nicht komprimiert, weil wir volle Schärfe wollten. Der Preis ist die Größe: Schon das ursprüngliche Spiel hatte im Editor 313 MB Texturen im Speicher, im App Store hat es 299 MB. Komprimierung würde die Texturen nach unserer Schätzung auf etwa 40 MB verkleinern. Wenn Ihnen die Spielgröße wichtig ist, sparen Sie hier zuerst.</p>
<p>Ein anderes Unity-Spiel von der ersten Codezeile bis zur Veröffentlichung beschreibt der Artikel über <a href="https://smallwater.eu/de/blog/shmup-apple-watch-devlog/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/dreamica-remaster-unity-urp-glow/" data-ga-destination="/de/blog/shmup-apple-watch-devlog/" data-ga-placement="inline">Space Impact Watch</a>, um Performance in Unity geht es im Artikel über <a href="https://smallwater.eu/de/blog/unity-dots-horde-navigation/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/dreamica-remaster-unity-urp-glow/" data-ga-destination="/de/blog/unity-dots-horde-navigation/" data-ga-placement="inline">Horden-Navigation in Unity DOTS</a>. Liegt das Spiel noch in Unity 4 vor, öffnen Sie es nicht direkt in Unity 6; den sicheren Weg beschreibt die Anleitung, <a href="https://smallwater.eu/de/blog/unity-4-to-unity-6-upgrade/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/dreamica-remaster-unity-urp-glow/" data-ga-destination="/de/blog/unity-4-to-unity-6-upgrade/" data-ga-placement="inline">wie Sie ein Unity-4-Projekt auf Unity 6 umstellen</a>. Wenn Sie ein altes Unity-Spiel haben, das aus den Stores verschwunden ist, und es zurückbringen möchten, <a href="https://smallwater.eu/de/#kontakt">melden Sie sich bei uns</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Wie man Fake-Ragdolls für Tausende Gegner in Unity DOTS baut</title>
      <link>https://smallwater.eu/de/blog/fake-ragdoll-unity-dots/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/fake-ragdoll-unity-dots/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Tower Defense</category>
      <description>Wie Sie Tausende Gegner in Unity DOTS ohne Knochenphysik wegschleudern: Körper als ein Punkt, deterministischer Wurf, begehbare Landung und Rotation.</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="Von einer Explosion weggeschleuderte Figuren fliegen auf Bögen und drehen sich in der Luft" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#warum-kein-echter-ragdoll" data-ga="blog_toc_clicked" data-ga-heading-id="warum-kein-echter-ragdoll">Warum kein echter Ragdoll</a></li><li><a href="#der-korper-als-ein-punkt" data-ga="blog_toc_clicked" data-ga-heading-id="der-korper-als-ein-punkt">Der Körper als ein Punkt</a></li><li><a href="#phasen-eines-umgeworfenen-gegners" data-ga="blog_toc_clicked" data-ga-heading-id="phasen-eines-umgeworfenen-gegners">Phasen eines umgeworfenen Gegners</a></li><li><a href="#wurf-hoher-nicht-weiter" data-ga="blog_toc_clicked" data-ga-heading-id="wurf-hoher-nicht-weiter">Wurf: höher, nicht weiter</a></li><li><a href="#landung-auf-einer-begehbaren-zelle" data-ga="blog_toc_clicked" data-ga-heading-id="landung-auf-einer-begehbaren-zelle">Landung auf einer begehbaren Zelle</a><ol><li><a href="#abprallen-an-wanden-in-der-luft-reicht-nicht" data-ga="blog_toc_clicked" data-ga-heading-id="abprallen-an-wanden-in-der-luft-reicht-nicht">Abprallen an Wänden in der Luft reicht nicht</a></li><li><a href="#vorhersage-der-flugbahn-beim-wurf" data-ga="blog_toc_clicked" data-ga-heading-id="vorhersage-der-flugbahn-beim-wurf">Vorhersage der Flugbahn beim Wurf</a></li></ol></li><li><a href="#rotation-nach-der-flughohe-geplant" data-ga="blog_toc_clicked" data-ga-heading-id="rotation-nach-der-flughohe-geplant">Rotation nach der Flughöhe geplant</a></li><li><a href="#das-modell-auf-den-boden-setzen" data-ga="blog_toc_clicked" data-ga-heading-id="das-modell-auf-den-boden-setzen">Das Modell auf den Boden setzen</a></li><li><a href="#tote-gegner-korper-decal-und-staub" data-ga="blog_toc_clicked" data-ga-heading-id="tote-gegner-korper-decal-und-staub">Tote Gegner: Körper, Decal und Staub</a></li><li><a href="#fallen-in-unity-entities" data-ga="blog_toc_clicked" data-ga-heading-id="fallen-in-unity-entities">Fallen in Unity Entities</a></li><li><a href="#leistung" data-ga="blog_toc_clicked" data-ga-heading-id="leistung">Leistung</a></li><li><a href="#grenzen-der-losung" data-ga="blog_toc_clicked" data-ga-heading-id="grenzen-der-losung">Grenzen der Lösung</a></li></ol></nav><p>Wenn eine Explosion im Spiel Hunderte Gegner gleichzeitig wegschleudert, ist ein echter Ragdoll für jeden einzelnen zu teuer. Einen Fake-Ragdoll in Unity DOTS bauen Sie, indem Sie den ganzen Körper des Gegners als einen einzigen Punkt mit eigener, einfacher Physik simulieren und das Modell nur um die Körpermitte drehen. Fallen und Aufstehen sind gewöhnliche Animationsclips, Knochen werden überhaupt nicht simuliert.</p>
<p>Wir zeigen das Vorgehen an einem Tower-Defense-Spiel für PC, das wir in Unity mit ECS entwickeln (<a href="https://smallwater.eu/de/#referencie">mehr in unseren Referenzen</a>). Eine Bombe oder ein Fireball schleudert dort Dutzende bis Hunderte Gegner auf einmal weg. Bei 2.000 Körpern in der Luft kostet die Simulation 0,53 bis 0,59 ms pro Tick (ein Tick ist ein Schritt der Spielsimulation mit fester Länge).</p>
<h2 id="warum-kein-echter-ragdoll"><a class="heading-anchor" href="#warum-kein-echter-ragdoll" aria-hidden="true" tabindex="-1">#</a>Warum kein echter Ragdoll</h2>
<p>Ein echter Ragdoll besteht pro Figur aus zehn oder mehr Physikkörpern, die über Gelenke verbunden sind. Bei Tausenden Figuren auf dem Bildschirm ist das nicht realistisch.</p>
<p>Der zweite Grund ist Determinismus: Derselbe Zustand muss dasselbe Ergebnis liefern. Unser Spiel braucht das, um eine verlorene Welle ab einem Checkpoint zu wiederholen, und wir prüfen es mit einem Hash des Simulationszustands nach 1.800 Ticks.</p>
<h2 id="der-korper-als-ein-punkt"><a class="heading-anchor" href="#der-korper-als-ein-punkt" aria-hidden="true" tabindex="-1">#</a>Der Körper als ein Punkt</h2>
<p>Verwenden Sie statt einer Physik-Engine eine eigene, einfache 2,5D-Physik: Position im Raster, Höhe über dem Boden, ballistischer Flug, Abprallen an Wänden und Reibung. In unserem Spiel bewegt sich auch die Bombe so, und ein von der Explosion getroffener Gegner wird für einen Moment zum gleichen Körper. Die Inspiration war, wie Soldaten in <a href="https://store.steampowered.com/app/1272320/Diplomacy_is_Not_an_Option/" target="_blank" rel="noopener">Diplomacy is Not an Option</a> durch die Luft fliegen.</p>
<p>Die Simulation kennt nur Position, Höhe und Drehwinkel. Schreiben Sie die Drehung des Modells nicht in die Simulation. Sie gehört in die zusätzliche Transformationsmatrix der Entity, die das Paket Entities (ECS in Unity) für nicht-uniforme Skalierung unterstützt (<a href="https://docs.unity3d.com/Packages/com.unity.entities@1.3/api/Unity.Transforms.PostTransformMatrix.html" target="_blank" rel="noopener">Dokumentation</a>).</p>
<h2 id="phasen-eines-umgeworfenen-gegners"><a class="heading-anchor" href="#phasen-eines-umgeworfenen-gegners" aria-hidden="true" tabindex="-1">#</a>Phasen eines umgeworfenen Gegners</h2>
<p>Ein umgeworfener Gegner durchläuft drei Phasen und kehrt dann in die Menge zurück:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">beim Treffer:
    berechne die Wurfgeschwindigkeit (Kraft, Abstand zur Explosion, Zufall aus dem Seed)
    finde eine Landung auf einer begehbaren Zelle (siehe unten)
    schalte den Zustand &quot;umgeworfen&quot; ein, gib den Platz am Angriffsziel frei
Flug:      der Körper fliegt auf einer Parabel, das Modell dreht sich um die Körpermitte
Landung:   Lage aus der Drehung (Rücken oder Bauch), kurzes Nachkippen
Liegen:    wartet eine feste Anzahl von Ticks
Aufstehen: Aufsteh-Clip vom Rücken oder vom Bauch, dann zurück in die Menge</code></pre>
<p>Bauen Sie den Zustand „umgeworfen“ als Komponente, die sich ein- und ausschalten lässt, ohne die Entity zwischen Chunks zu verschieben (Speicherblöcken, in denen ECS Entities mit derselben Komponentenkombination ablegt). Navigation, Separation in der Menge und Angriffsentscheidungen überspringen umgeworfene Gegner dann einfach. Wie Navigation und Separation in einer großen Menge funktionieren, beschreibt der Artikel über <a href="https://smallwater.eu/de/blog/unity-dots-horde-navigation/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/fake-ragdoll-unity-dots/" data-ga-destination="/de/blog/unity-dots-horde-navigation/" data-ga-placement="inline">Horden-Navigation in Unity DOTS</a>.</p>
<h2 id="wurf-hoher-nicht-weiter"><a class="heading-anchor" href="#wurf-hoher-nicht-weiter" aria-hidden="true" tabindex="-1">#</a>Wurf: höher, nicht weiter</h2>
<p>Der einfachste Wurf geht in einem Winkel von 45° ± 10°. Bei einer Bombenkraft von 18 m/s und einer Gravitation von 20 m/s² liegt der Scheitelpunkt bei etwa 4 m. Der Körper fliegt tief über die Menge, und der Effekt geht darin unter.</p>
<p>Mehr Kraft hilft nicht, weil damit auch die Weite wächst. Teilen Sie die Geschwindigkeit in zwei Komponenten und skalieren Sie jede separat: Winkel 60° ± 5°, horizontale Komponente × 0,85, vertikale × 1,3. Der Scheitelpunkt liegt dann bei etwa 10 m, die Weite bleibt bei etwa 15,5 m, und der Flug dauert ungefähr 2 Sekunden.</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="Seitenansicht: ein niedriger gestrichelter Bogen und ein hoher Bogen über ein Haus mit Silhouetten einer Figur, die sich im Flug dreht" width="2200" height="1520" loading="lazy" decoding="async" /></picture><figcaption>Gestrichelt ein Wurf mit 45°, durchgezogen ein Wurf mit 60° und getrennten Faktoren. Das Haus ist 5 m hoch.</figcaption></figure>
<p>Die Richtung weicht um ± 15° von der Explosion ab, die Kraft um ± 20 %, und am Rand der Explosion sinkt sie auf die Hälfte. Nehmen Sie den Zufall nicht aus einem globalen Generator, sondern aus einem Hash von Level-Seed, Kennung des Gegners und laufender Nummer der Explosion. Verarbeiten Sie die Umwerf-Anfragen sortiert, denn Jobs auf mehreren Threads erzeugen sie in beliebiger Reihenfolge. Zwei Durchläufe derselben Welle ergeben so dieselben Bögen.</p>
<p>Umwerfen ist ja oder nein. Jeder umwerfbare Gegner fällt bei einem Treffer mindestens um (die Geschwindigkeit ist nach unten auf 2 m/s begrenzt), die Resistenz ändert nur die Weite. Geben Sie Immunität ein eigenes Flag, in unserem Spiel hat es der größte Gegnertyp. Ergäbe sich die Immunität aus der Resistenz (bei 0,95 liegt die Geschwindigkeit unter 2 m/s) und würden schwache Würfe verworfen, wäre der Gegner nur zufällig immun.</p>
<h2 id="landung-auf-einer-begehbaren-zelle"><a class="heading-anchor" href="#landung-auf-einer-begehbaren-zelle" aria-hidden="true" tabindex="-1">#</a>Landung auf einer begehbaren Zelle</h2>
<p>Ein Gegner darf nicht in einer Schlucht, auf einer Klippe oder in der Grundfläche eines Turms landen. Begehbarkeit wird pro Rasterzelle und für jede Größenklasse von Gegnern separat bestimmt.</p>
<h3 id="abprallen-an-wanden-in-der-luft-reicht-nicht"><a class="heading-anchor" href="#abprallen-an-wanden-in-der-luft-reicht-nicht" aria-hidden="true" tabindex="-1">#</a>Abprallen an Wänden in der Luft reicht nicht</h3>
<p>Naheliegend ist, nicht begehbare Zellen als Wände zu behandeln, von denen der fliegende Körper abprallt. An einer Klippe sieht das natürlich aus, an einer Schlucht nicht: Über einer Schlucht ist Luft, aber die Zelle ist nicht begehbar, also prallt der Körper an einer unsichtbaren Säule über ihrem Rand ab. Außerdem garantiert nichts, dass er nach dem Abprall an einer guten Stelle landet.</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="Seitenansicht: Ein Körper fliegt über eine Schlucht, prallt mitten im Flug an einer unsichtbaren Säule über ihrem Rand ab und fällt zurück" width="2200" height="1640" loading="lazy" decoding="async" /></picture><figcaption>Abprall an einer nicht begehbaren Zelle. Der Körper prallt an der Luft über dem Rand der Schlucht ab.</figcaption></figure>
<h3 id="vorhersage-der-flugbahn-beim-wurf"><a class="heading-anchor" href="#vorhersage-der-flugbahn-beim-wurf" aria-hidden="true" tabindex="-1">#</a>Vorhersage der Flugbahn beim Wurf</h3>
<p>Zuverlässiger ist es, die Landung gleich beim Wurf zu bestimmen. Simulieren Sie den Flug im Voraus mit demselben Schritt wie die echte Physik und prüfen Sie die Landezelle für die Größenklasse des Gegners. Ist sie nicht begehbar, probieren Sie weitere Varianten in fester Reihenfolge:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">für Winkel in [0, +25, -25, +50, -50, ... , +175, -175]:
    für Faktor der horizontalen Geschwindigkeit in [1, 0.7, 0.4, 0.1]:
        simuliere den Flug
        wenn die Landezelle für die Klasse des Gegners begehbar ist: nimm diese Richtung und Ende
wenn nichts passt: horizontale Geschwindigkeit = 0 (Hüpfer auf der Stelle)</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="Draufsicht auf ein Raster mit Schlucht und erhöhter Klippe, durchgestrichene verworfene Landungen und die erste begehbare hervorgehoben" width="2200" height="1400" loading="lazy" decoding="async" /></picture><figcaption>Explosion, Gegner, verworfene Landungen und die gewählte. Die grauen Punkte sind alle Kandidaten für die Landung.</figcaption></figure>
<p>Im schlimmsten Fall sind das 60 Flugsimulationen pro Gegner (15 Richtungen × 4 Faktoren). Die vertikale Geschwindigkeit ändert sich nicht, die Flughöhe bleibt also gleich, nur die Weite wird kürzer. Trifft den Gegner während des Flugs eine weitere Explosion, starten Sie die Vorhersage erneut.</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="Illustration in 2D: Zwei Gegner würden in der Schlucht landen (Kreuze), die Vorhersage verkürzt ihren Flug."><source src="https://smallwater.eu/blog/fake-ragdoll-unity-dots/ragdoll-loop.mp4" type="video/mp4" /></video><figcaption>Illustration in 2D: Zwei Gegner würden in der Schlucht landen (Kreuze), die Vorhersage verkürzt ihren Flug.</figcaption></figure>
<p>Damit der Körper nach der Landung weder rutscht noch abprallt, geben Sie ihm einen Restitutionskoeffizienten am Boden von 0 und hohe Reibung. Er bleibt dort liegen, wo die Vorhersage es gesagt hat.</p>
<h2 id="rotation-nach-der-flughohe-geplant"><a class="heading-anchor" href="#rotation-nach-der-flughohe-geplant" aria-hidden="true" tabindex="-1">#</a>Rotation nach der Flughöhe geplant</h2>
<p>Am einfachsten ist es, das Modell mit zufälliger Geschwindigkeit zu drehen, etwa 180 bis 540 °/s, und es nach der Landung innerhalb von 0,2 s in die nächste Liegeposition kippen zu lassen. Das Nachkippen ist aber sichtbar, wenn der Körper fast aufrecht landet. Die Drehgeschwindigkeit in jedem Tick so nachzuregeln, dass der Winkel bei der Landung genau waagerecht ist, hilft nur teilweise: Auch ein niedriger Wurf dreht sich wie ein Salto, und die Drehung hat nichts damit zu tun, wie hoch der Körper fliegt.</p>
<p>Besser ist es, die Rotation beim Wurf aus der Flughöhe zu planen:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">Höhe = vz² / (2 g), Flugzeit aus vz und Höhe
Ziel = nächste waagerechte Lage in zufälliger Richtung (Vierteldrehung aus dem Stand)
Ziel += halbe Drehung je 2,5 m Höhe, höchstens 3
Drehgeschwindigkeit = (Ziel - aktueller Winkel) / Flugzeit</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="Zwei Bögen: ein niedriger Wurf, bei dem die Figur nur umkippt, und ein hoher, bei dem sie dreieinviertel Drehungen macht" width="2200" height="1200" loading="lazy" decoding="async" /></picture><figcaption>Ein niedriger Wurf (Scheitel etwa 2 m) dreht um 90°, eine Bombe (etwa 10 m) um 90° plus drei halbe Drehungen.</figcaption></figure>
<p>Die Landeseite folgt aus dem Winkel: Gesicht zum Boden heißt Bauch, sonst Rücken. Danach wird der Aufsteh-Clip gewählt. Ein liegender oder aufstehender Gegner behält bei einem weiteren Treffer seinen aktuellen Winkel und springt nicht in die aufrechte Lage zurück.</p>
<h2 id="das-modell-auf-den-boden-setzen"><a class="heading-anchor" href="#das-modell-auf-den-boden-setzen" aria-hidden="true" tabindex="-1">#</a>Das Modell auf den Boden setzen</h2>
<p>Das Modell dreht sich um die Körpermitte, der Boden liegt aber unter seinem tiefsten Punkt, und der ändert sich mit der Drehung. Eine Schätzung des Versatzes aus zwei Konstanten und dem Sinus des Winkels passt nicht zu Modellen mit weit ausgebreiteten Armen im Fall-Clip: In unserem Spiel sank das Skelett-Modell (ein häufiger Gegner) bis zu 1,15 m in den Boden ein, das Modell des Nekromanten bis zu 1,73 m.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Schätzung</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="Sechs Drehungen einer Figur, die Teile unter dem Boden sind rot" width="2200" height="840" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Gemessen</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="Sechs Drehungen einer Figur, jede berührt den Boden mit ihrem tiefsten Punkt" width="2200" height="840" loading="lazy" decoding="async" /></picture></div></div><figcaption>Dieselben Drehungen mit Versatz aus Konstanten und aus der gemessenen Tabelle.</figcaption></figure>
<p>Zuverlässiger ist es, den tiefsten Punkt zu messen. Berechnen Sie beim Baken der Animationen für den Fall-Clip den tiefsten Punkt des Körpers bei 72 Drehungen in 5°-Schritten und interpolieren Sie zur Laufzeit aus der Tabelle. Das tiefste Einsinken fällt so auf 4 mm. Ein einziges Minimum über alle Frames des Clips reicht allerdings nicht: Der Körper würde nach der Landung 0,15 bis 0,28 m über dem Boden schweben, bis der Aufsteh-Clip ihn herunterzieht. Die Tabelle muss deshalb die Dimension Frames × Drehungen haben.</p>
<p>Die langsame geplante Drehung zeigt noch einen Effekt. Setzen Sie das Modell mit seinem tiefsten Punkt auf die Flughöhe, sinkt die Körpermitte beim Übergang vom Stehen in die Waagerechte um die Differenz zwischen halber Höhe und halber Dicke der Figur. Der Körper steigt dann am Anfang des Flugs nur langsam und beschleunigt erst danach. Heben Sie das Modell deshalb um die Differenz zwischen seiner halben Ausdehnung im Stand und beim aktuellen Winkel an, höchstens um die Flughöhe. Die Körpermitte folgt dann der Parabel.</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="Diagramm der Höhe der Körpermitte über die Zeit: eine gestrichelte ideale Parabel, eine rote Kurve mit Delle und eine blaue, die der Parabel folgt" width="2200" height="1040" loading="lazy" decoding="async" /></picture><figcaption>Höhe der Körpermitte während des Flugs: ohne Anhebung (rot) und mit Anhebung (blau).</figcaption></figure>
<h2 id="tote-gegner-korper-decal-und-staub"><a class="heading-anchor" href="#tote-gegner-korper-decal-und-staub" aria-hidden="true" tabindex="-1">#</a>Tote Gegner: Körper, Decal und Staub</h2>
<p>Ein von einer Explosion getöteter Gegner soll ebenfalls fliegen. Erzeugen Sie dafür einen fliegenden Körper: eine Kopie der Entity ohne Gameplay-Komponenten (Lebenspunkte, Belohnung, Ziel). Der Körper fliegt, landet, liegt eine Weile und verschwindet. An seiner Stelle bleiben ein Leichen-Decal (eine auf den Boden projizierte Textur) wie in Warcraft 3 und eine kleine Staubwolke. In unserem Spiel liegt die Obergrenze bei 1.000 fliegenden Körpern und 1.500 Decals.</p>
<p>Das Decal ist eine Draufsicht auf einen Frame des Fall-Clips, gedreht wie der liegende Körper, sodass der Tausch von Körper zu Decal nicht zu sehen ist. Der erste Frame des Aufsteh-Clips eignet sich nicht, weil die Figur darin vom Rücken schon aufgerichtet ist. Fassen Sie Staubwolken in einer Zelle von 1,5 m zusammen. Eine Bombe in die Menge erzeugt so ein paar größere Wolken statt Hunderter kleiner.</p>
<h2 id="fallen-in-unity-entities"><a class="heading-anchor" href="#fallen-in-unity-entities" aria-hidden="true" tabindex="-1">#</a>Fallen in Unity Entities</h2>
<ul>
<li><strong>Die Matrix aus dem Baker verschwindet.</strong> Wenn Sie die zusätzliche Matrix beim Baken hinzufügen, entfernt das Transform-System sie bei uniformer Skalierung, und umgeworfene Gegner bewegen sich nicht. Fordern Sie beim Baken deshalb nicht-uniforme Skalierung an. Dann wandert allerdings die gesamte Skalierung des Prefabs in dieselbe Matrix, überschreiben Sie sie also nicht mit der reinen Rotation. Sonst hat ein umgeworfener Gegner mit Skalierung 2× nur die halbe Größe, auch nach dem Aufstehen. Speichern Sie die Skalierung separat und setzen Sie die Matrix aus Skalierung und Rotation zusammen.</li>
<li><strong>Wurzelhöhe beim Import der Clips.</strong> Bei Humanoid-Clips wird die vertikale Bewegung der Wurzel (Root) verworfen, und Figuren fallen beim Aufstehen durch den Boden. Baken Sie die Wurzelhöhe in die Pose. Die Option, die Pose an den Füßen zu verankern, sollten Sie nicht verwenden: Sie verschiebt den ganzen Körper um einen Meter nach unten.</li>
<li><strong>Matrix in geschlossener Form.</strong> Verschiebung, Rotation und Rückverschiebung als drei Matrixmultiplikationen sind im Job messbar langsamer als eine von Hand geschriebene Formel für die resultierende Matrix.</li>
<li><strong>Burst mit alter Version.</strong> Nachdem Sie die Felder einer Komponente geändert haben, die ein Burst-Job liest, kann der Editor abstürzen oder Unsinn melden. Ein Neustart des Editors hilft.</li>
</ul>
<h2 id="leistung"><a class="heading-anchor" href="#leistung" aria-hidden="true" tabindex="-1">#</a>Leistung</h2>
<p>Gemessen im Unity Editor auf einem M3 Pro, 10.000 Skelette in einer Benchmark-Szene, Wurfkraft 18. Die Zeiten sind die Summe der CPU-Zeit der Burst-Jobs über alle Threads, nicht die Frame-Zeit. Diagramm und Tabelle sind ohne Flugbahnvorhersage und Landetabellen.</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="Diagramm der Zeit pro Tick nach Anzahl umgeworfener Gegner für Simulation und Vorbereitung der Darstellung, mit Ziellinie bei 0,5 ms" width="2200" height="1120" loading="lazy" decoding="async" /></picture><figcaption>Simulation und Vorbereitung der Darstellung ohne Flugbahnvorhersage. Ziel 0,5 ms bei 2.000 umgeworfenen.</figcaption></figure>
<div class="table-wrap"><table>
<thead>
<tr>
<th>umgeworfen</th>
<th>Simulation (ms/Tick)</th>
<th>Vorbereitung der Darstellung (ms)</th>
</tr>
</thead>
<tbody><tr>
<td>500</td>
<td>0,28 bis 0,30</td>
<td>0,15</td>
</tr>
<tr>
<td>2.000</td>
<td>0,53</td>
<td>0,30 bis 0,55</td>
</tr>
<tr>
<td>5.000</td>
<td>0,74 bis 0,75</td>
<td>0,49 bis 0,91</td>
</tr>
<tr>
<td>10.000</td>
<td>0,96 bis 1,01</td>
<td>0,68 bis 1,24</td>
</tr>
</tbody></table>
</div>
<ul>
<li>Die Simulation wächst linear, etwa um 0,25 ms pro tausend umgeworfene Gegner. Mit Flugbahnvorhersage und Landetabellen sind es bei 2.000 umgeworfenen 0,58 bis 0,59 ms, also knapp über dem Ziel von 0,5 ms. Eine sinnvolle Obergrenze sind daher 3.000 gleichzeitig umgeworfene Gegner, das sind etwa 0,8 ms.</li>
<li>Eine Explosion in eine Gruppe von 359 Gegnern an einer Wand kostet im System der Umwerf-Anfragen 1,3 bis 1,9 ms in einem einzigen Frame. Den größten Teil davon macht die Flugbahnvorhersage aus.</li>
<li>FPS aus dem Editor lassen sich nicht vergleichen: Zwei Durchläufe derselben Konfiguration unterschieden sich um bis zu 80 FPS.</li>
</ul>
<h2 id="grenzen-der-losung"><a class="heading-anchor" href="#grenzen-der-losung" aria-hidden="true" tabindex="-1">#</a>Grenzen der Lösung</h2>
<p>Die Vorbereitung der Darstellung stieg in einer anderen Testszene auf bis zu 1,3 ms, vermutlich wegen des Lesens der Landetabelle für jeden Körper. Das ist der erste Kandidat für eine Optimierung. FPS in einem Build und die Last beim gleichzeitigen Töten von 1.000 Gegnern (Ziel: höchstens 1,5 ms zusätzlich pro Frame) sind noch nicht gemessen.</p>
<p>Wenn Sie an einer ähnlichen Simulation von Menschenmengen oder Effekten in Unity arbeiten und Hilfe brauchen, <a href="https://smallwater.eu/de/#kontakt">schreiben Sie uns</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Wie man einen Shmup für die Apple Watch entwickelt</title>
      <link>https://smallwater.eu/de/blog/shmup-apple-watch-devlog/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/shmup-apple-watch-devlog/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Space Impact Watch</category>
      <description>Anleitung für einen Shmup auf der Apple Watch: virtuelle Auflösung, Digital Crown, Kollisionen mit Rechtecken, Pooling, Level als Zeittabellen, Generator.</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="Pixelszene auf grünem Display: links ein Schiff, das auf Kometen, Raketen und einen Boss rechts schießt" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#feste-virtuelle-auflosung-84-48" data-ga="blog_toc_clicked" data-ga-heading-id="feste-virtuelle-auflosung-84-48">Feste virtuelle Auflösung 84 × 48</a></li><li><a href="#steuerung-mit-der-krone-und-automatisches-feuer" data-ga="blog_toc_clicked" data-ga-heading-id="steuerung-mit-der-krone-und-automatisches-feuer">Steuerung mit der Krone und automatisches Feuer</a></li><li><a href="#der-game-loop-in-einer-szene" data-ga="blog_toc_clicked" data-ga-heading-id="der-game-loop-in-einer-szene">Der Game Loop in einer Szene</a></li><li><a href="#kollisionen-mit-rechtecken-statt-physik-engine" data-ga="blog_toc_clicked" data-ga-heading-id="kollisionen-mit-rechtecken-statt-physik-engine">Kollisionen mit Rechtecken statt Physik-Engine</a></li><li><a href="#pooling-nur-dort-wo-es-sich-lohnt" data-ga="blog_toc_clicked" data-ga-heading-id="pooling-nur-dort-wo-es-sich-lohnt">Pooling nur dort, wo es sich lohnt</a></li><li><a href="#level-als-zeittabellen" data-ga="blog_toc_clicked" data-ga-heading-id="level-als-zeittabellen">Level als Zeittabellen</a></li><li><a href="#endlosmodus-aus-einer-pseudozufallszahl" data-ga="blog_toc_clicked" data-ga-heading-id="endlosmodus-aus-einer-pseudozufallszahl">Endlosmodus aus einer Pseudozufallszahl</a></li><li><a href="#grenzen-dieses-ansatzes" data-ga="blog_toc_clicked" data-ga-heading-id="grenzen-dieses-ansatzes">Grenzen dieses Ansatzes</a></li></ol></nav><p>Ein Shmup (Shoot &#39;em up, ein Shooter, in dem das eigene Schiff gegen Wellen von Gegnern fliegt) auf der Apple Watch hat drei Probleme: ein kleines Display, einen Finger, der das halbe Spiel verdeckt, und Uhren in verschiedenen Größen. Drei Entscheidungen lösen sie: Das ganze Spiel läuft auf einem festen Raster von 84 × 48 Punkten, Kollisionen werden von Hand mit Rechtecken geprüft, und Level sind Tabellen mit Zeitpunkten statt Code. Gesteuert wird mit der Digital Crown, geschossen wird automatisch.</p>
<p>Die Beispiele stammen aus <a href="https://smallwater.eu/space-impact-watch">Space Impact Watch</a>, einem Remake des Shooters Space Impact, der im Jahr 2000 auf dem Nokia 3310 erschien. Das Spiel läuft auf der Uhr sowie auf iPhone und iPad. Technisch basiert es auf SpriteKit, dem 2D-Spieleframework von Apple, die Prinzipien gelten aber für jeden Shmup auf einem kleinen Bildschirm.</p>
<h2 id="feste-virtuelle-auflosung-84-48"><a class="heading-anchor" href="#feste-virtuelle-auflosung-84-48" aria-hidden="true" tabindex="-1">#</a>Feste virtuelle Auflösung 84 × 48</h2>
<p>Legen Sie zuerst fest, in welchen Einheiten das Spiel rechnet. Mit Bildschirmpunkten ist die Welt auf jeder Uhrengröße anders groß. Besser ist eine kleine virtuelle Auflösung, in der alles berechnet wird. In unserem Spiel sind das 84 × 48, genau wie beim <a href="https://www.adafruit.com/products/338" target="_blank" rel="noopener">Display des Nokia 3310</a>. Positionen, Geschwindigkeiten, Spritegrößen und Flugbahnen der Gegner sind in diesen Einheiten angegeben. Beim Start der Szene wird einmal berechnet, wie viele Bildschirmpunkte ein virtuelles Pixel hat (Szenenbreite geteilt durch 84), und alles wird nur damit multipliziert.</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="Spielszene mit sichtbarem Raster aus 84 × 48 virtuellen Pixeln" width="1008" height="576" loading="lazy" decoding="async" /></picture><figcaption>Ein Level auf dem Raster 84 × 48. Jedes Quadrat ist ein virtuelles Pixel, das die Uhr als Block aus mehreren echten Punkten zeichnet.</figcaption></figure>
<p>Was Sie damit gewinnen:</p>
<ul>
<li><strong>Gleiches Aussehen auf jedem Gerät.</strong> Das Spiel sieht auf jeder Uhrengröße und auf dem iPad gleich aus, nur die Blockgröße ändert sich.</li>
<li><strong>Leveldesign mit kleinen ganzen Zahlen.</strong> Ein Gegner fliegt auf Höhe 13, eine Rakete biegt bei x = 20 ab.</li>
<li><strong>Einfache Grafik.</strong> Die Sprites sind einfarbig und in zehnfacher Größe gezeichnet, ein virtuelles Pixel ist im Quellbild also ein Block von 10 × 10. Ihre Farbe bekommen sie erst im Spiel: in hellen Leveln schwarz auf grünem Hintergrund, in dunklen umgekehrt.</li>
</ul>
<h2 id="steuerung-mit-der-krone-und-automatisches-feuer"><a class="heading-anchor" href="#steuerung-mit-der-krone-und-automatisches-feuer" aria-hidden="true" tabindex="-1">#</a>Steuerung mit der Krone und automatisches Feuer</h2>
<p>Auf der Uhr verdeckt ein Finger das halbe Spiel, deshalb wird das Schiff nicht per Touch gesteuert. Die Bewegung übernimmt die Digital Crown: Eine Drehung bewegt das Schiff nach oben oder unten. Bieten Sie eine Einstellung für die Empfindlichkeit und eine Option zum Umkehren der Richtung an.</p>
<p>Geschossen wird automatisch, in unserem Spiel alle 0,4 Sekunden, ab Level vier alle 0,3. Das Display dient nur für eine Geste: Ein Wischen nach rechts löst eine von drei Spezialwaffen aus.</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="Drei Spezialwaffen: eine senkrechte Linie, eine Lenkrakete und ein Laser, der einen Boss trifft" width="1104" height="240" loading="lazy" decoding="async" /></picture><figcaption>Drei Spezialwaffen: eine Linie über die ganze Höhe des Spielfelds, eine Lenkrakete und ein Laser.</figcaption></figure>
<p>Auf der Uhr ist die Vibration bei einem Treffer genauso wichtig wie die Animation, eine kleine Explosion auf dem Display übersieht man leicht. Lösen Sie Haptik über eine einzige Schnittstelle aus (Treffer, Gegner zerstört, Boss besiegt), die jede Plattform selbst umsetzt: die Uhr mit einer Vibration, das iPhone auf seine eigene Art. Die Telefonversion hat keine Krone, sondern Tasten auf dem Display, die dieselben Einstellungen für Empfindlichkeit und Richtung verwenden.</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 des Spiels im Apple-Watch-Simulator, das Schiff vor einem Boss" width="368" height="448" loading="lazy" decoding="async" /></picture><figcaption>Das Spiel im Apple-Watch-Simulator: das Schiff vor einem Boss.</figcaption></figure>
<h2 id="der-game-loop-in-einer-szene"><a class="heading-anchor" href="#der-game-loop-in-einer-szene" aria-hidden="true" tabindex="-1">#</a>Der Game Loop in einer Szene</h2>
<p>Das ganze Spiel kann eine Szene mit einer Schleife sein:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">in jedem Frame:
    alle Objekte bewegen
    Kollisionen prüfen
    je nach Zeit im Level neue Gegner erzeugen</code></pre>
<p>Bauen Sie die Teile in der Reihenfolge ein, in der sie sich am frühesten testen lassen:</p>
<ul>
<li><strong>Schießen und Bewegung des Spielers.</strong> Ohne sie lässt sich nichts testen.</li>
<li><strong>Gegner und Kollisionen.</strong> Dem ersten Gegner reicht es, geradeaus nach links zu fliegen und nach einem Treffer zu sterben.</li>
<li><strong>Vibration und Trefferanimationen.</strong> Ohne sie merkt der Spieler auf dem kleinen Display nicht, dass er getroffen hat.</li>
<li><strong>Hintergrund und HUD.</strong> Leben, Punktestand, Anzahl der Spezialwaffen. Wenn das HUD in einem Level stört, verschieben Sie es nach unten.</li>
</ul>
<p>Am Ende steht eine spielbare Schleife mit Leben, Punktestand, Explosionen, Hintergrund und Respawn des Spielers. Wenn Sie die Spiellogik von Darstellung und Eingabe trennen, können Uhren- und Telefonversion sie gemeinsam nutzen.</p>
<h2 id="kollisionen-mit-rechtecken-statt-physik-engine"><a class="heading-anchor" href="#kollisionen-mit-rechtecken-statt-physik-engine" aria-hidden="true" tabindex="-1">#</a>Kollisionen mit Rechtecken statt Physik-Engine</h2>
<p>SpriteKit hat eine Physik-Engine, und es liegt nahe, sie auch für Kollisionen zu nutzen. Jedes Objekt bekommt einen Physikkörper aus dem Umriss seines Sprites, präzise Kollisionserkennung und eine Kategorie (Spieler, Gegner, Geschoss, Spezialwaffe), und die Engine meldet selbst die Paare, die sich berührt haben. Apple schreibt in der <a href="https://developer.apple.com/documentation/spritekit/getting-started-with-physics-bodies" target="_blank" rel="noopener">Dokumentation zu Physikkörpern</a>, dass Körper aus dem Alphakanal einer Textur am genauesten und zugleich am rechenintensivsten sind. Wir haben das in unserem Spiel ausprobiert und sind zur Prüfung von Hand zurückgekehrt.</p>
<p>Die Prüfung von Hand ist eine Funktion, die nacheinander Paare von Gruppen durchgeht und Rechtecke vergleicht:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">für jedes Paar von Gruppen:
    (Spieler und Gegner, Spieler und Geschosse, Geschosse und Gegner, Geschosse und Geschosse)
    für jedes Objekt A der ersten Gruppe und jedes Objekt B der zweiten:
        wenn Rechteck A sich mit Rechteck B überlappt:
            Treffer auflösen</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="Zwei Sprite-Paare mit markierten Kollisionsrechtecken, links überlappen sich nur die Ecken der Rechtecke" width="960" height="352" loading="lazy" decoding="async" /></picture><figcaption>Links: Die Rechtecke überlappen sich, die Pixel nicht, das Spiel wertet es als Treffer. Rechts: ein echter Treffer.</figcaption></figure>
<p>Die Vorteile: Die Reihenfolge der Prüfungen liegt in Ihrer Hand, und die ganze Regel „was bei einem Treffer passiert“ steht an einer Stelle. Die Ungenauigkeit ist gering, denn auf dem Raster 84 × 48 sind die Sprites so klein, dass Rechteck und echter Umriss nur um wenige virtuelle Pixel abweichen.</p>
<h2 id="pooling-nur-dort-wo-es-sich-lohnt"><a class="heading-anchor" href="#pooling-nur-dort-wo-es-sich-lohnt" aria-hidden="true" tabindex="-1">#</a>Pooling nur dort, wo es sich lohnt</h2>
<p>Pooling bedeutet, dass Sie ein Objekt nach seinem Ende nicht löschen, sondern ausblenden und beim nächsten Mal wiederverwenden. Es ist verlockend, alles zu poolen, auch Explosionen und Power-ups. Das hat aber seinen Preis: Jede Kollision und jedes Update muss zuerst prüfen, ob das Objekt überhaupt lebt. Eine vergessene Prüfung reicht, und ein ausgeblendeter Gegner tötet den Spieler oder ein ausgeblendetes Geschoss schießt eine Rakete ab. Als wir in unserem Spiel das Pooling überall entfernt haben, waren es vor allem diese Prüfungen, die wegfielen.</p>
<p>Eine einfache Regel: Poolen Sie, wovon es viel gibt und was immer gleich entsteht. Der Spieler erzeugt beim Start 21 Geschosse und recycelt sie danach nur noch. Gegner und ihre Geschosse werden ebenfalls wiederverwendet, Explosionen und Power-ups entstehen und verschwinden normal.</p>
<h2 id="level-als-zeittabellen"><a class="heading-anchor" href="#level-als-zeittabellen" aria-hidden="true" tabindex="-1">#</a>Level als Zeittabellen</h2>
<p>Ein Level muss kein Code sein, eine Liste von Einträgen genügt. Jeder Eintrag sagt, was erscheint, in welcher Sekunde, auf welcher Höhe und wie es sich bewegt. Die Bewegung ist eine Liste von Punkten, die der Gegner mit gleichbleibender Geschwindigkeit abfliegt. Optional kommen Schusszeiten, ein Sturmangriff des Bosses oder ein Erzeuger kleinerer Gegner dazu.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">in jedem Frame:
    Zeit = jetzt - Levelstart - Zeit in der Pause
    für jeden Eintrag in der Leveltabelle:
        wenn der Eintrag unbenutzt ist und Zeit &gt; Eintrag.Zeit:
            Objekt laut Eintrag erzeugen
            Eintrag als benutzt markieren</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="Zeitachse des ersten Levels: von links nach rechts Sekunden, die Höhe der Symbole entspricht der Höhe, in der der Gegner erscheint" width="1004" height="120" loading="lazy" decoding="async" /></picture><figcaption>Level eins: 39 Einträge in 67 Sekunden. Kometen, dann Raketenwellen, Power-ups (rot) und am Ende der Boss.</figcaption></figure>
<p>Ein so geschriebenes Level lässt sich leicht abstimmen: Eine Welle um eine halbe Sekunde zu verschieben heißt, eine Zahl zu ändern. Auf der Zeitachse sieht man auch den Rhythmus, um den es geht: kurze Welle, Pause, anderer Gegnertyp, Power-up vor einem schwereren Abschnitt.</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="Eine Rakete fliegt im Zickzack über das Spielfeld, die Knickpunkte der Flugbahn sind markiert" width="1008" height="576" loading="lazy" decoding="async" /></picture><figcaption>Flugbahn einer Rakete aus Level eins: sechs Punkte, die sie mit gleichbleibender Geschwindigkeit abfliegt.</figcaption></figure>
<p>Der Nachteil zeigt sich bei größeren Änderungen. Das dichteste Level unseres Spiels hat 71 Zeilen, und so eine Tabelle liest sich ohne Editor schlecht. Günstiger ist es, Level als Wellen zu schreiben (eine Gruppe von Gegnern mit einem gemeinsamen Bewegungsmuster), nicht als einzelne Schiffe.</p>
<h2 id="endlosmodus-aus-einer-pseudozufallszahl"><a class="heading-anchor" href="#endlosmodus-aus-einer-pseudozufallszahl" aria-hidden="true" tabindex="-1">#</a>Endlosmodus aus einer Pseudozufallszahl</h2>
<p>Für eine Rangliste (bei uns Game Center) brauchen Sie einen Modus ohne Ende. Dort ersetzt ein Generator die handgeschriebene Tabelle und erzeugt sie während des Spiels. Er erzeugt eine 20-stellige Pseudozufallszahl, und einzelne Ziffernpaare entscheiden über eine Welle:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">Zahl = nächste 20-stellige Pseudozufallszahl
Typ = Power-up, wenn Ziffern 4 bis 5 &gt; 90, sonst Gegner
Sprite = Ziffern 1 bis 2 modulo Anzahl der Gegnerarten
Pause vor der Welle = Ziffer 3 / 2 Sekunden (mindestens 1)
Höhe = Ziffer 7, umgerechnet auf die Höhe des Spielfelds
Bewegungsmuster = Ziffern 10 bis 11 modulo 10
Schiffe in der Welle = Ziffer 17 / 2 + 1
Geschwindigkeit = Ziffer 13 × (1 + Nummer der Welle / 10), begrenzt auf 20 bis 50
jede dreißigste Welle = Boss, die Erzeugung ruht, solange der Boss lebt</code></pre>
<p>Lassen Sie den Seed (Startwert) des Generators im Spiel fest. Dann bekommt jeder Spieler dieselbe Wellenfolge, und die Punktzahlen in der Rangliste sind vergleichbar.</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="Zehn kleine Spielfelder, auf jedem ein Bewegungsmuster eines Gegners als gestrichelte Linie" width="912" height="228" loading="lazy" decoding="async" /></picture><figcaption>Zehn Bewegungsmuster aus dem Generator. Die genaue Form hängt von der Höhe ab, in der die Welle erscheint.</figcaption></figure>
<p>Wie sich der Modus entwickelt, zeigt eine Simulation des Generators außerhalb des Spiels (300 Wellen, derselbe Seed wie im Spiel). Der erste Boss kommt nach 29 Wellen, also nach 74 Gegnern und 3 Power-ups, nach etwa 130 Sekunden. Die ersten 30 Wellen fliegen mit der Mindestgeschwindigkeit 20, weil eine einzelne Ziffer noch unter der Untergrenze liegt. Danach steigt die Durchschnittsgeschwindigkeit auf 28,5, 37,5 und 41, ab Welle 120 sind die meisten Gegner am Maximum. Etwa jede elfte Welle ist ein Power-up.</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="Zeitachse der ersten 135 Sekunden des Endlosmodus mit Gegnersymbolen und einem Boss am Ende" width="1104" height="120" loading="lazy" decoding="async" /></picture><figcaption>Die ersten 135 Sekunden des Endlosmodus laut Simulation des Generators. Die Wellen sind länger und unregelmäßiger als in einem handgeschriebenen Level.</figcaption></figure>
<p>Die Schwäche: Der Generator kennt keinen Rhythmus. Ein handgeschriebenes Level wechselt zwischen Druck und Erholung, der Generator nicht. Die Pause während des Bosses und die steigende Geschwindigkeit helfen etwas, aber die handgeschriebenen Level spielen sich besser.</p>
<h2 id="grenzen-dieses-ansatzes"><a class="heading-anchor" href="#grenzen-dieses-ansatzes" aria-hidden="true" tabindex="-1">#</a>Grenzen dieses Ansatzes</h2>
<ul>
<li><strong>Keine automatischen Tests.</strong> Alles wird von Hand im Simulator und auf der Uhr geprüft.</li>
<li><strong>Globaler Zustand.</strong> Der Großteil des Spielzustands sind globale Variablen einer einzigen Szene. Für ein reines Uhrenspiel ist das bequem, beim Hinzufügen einer iPhone-Version nicht mehr.</li>
<li><strong>Kein Leveleditor.</strong> Eine Zeittabelle lässt sich in kleinen Schritten gut abstimmen, für größere Änderungen bräuchte sie einen Editor.</li>
</ul>
<p>Wenn Sie ein Spiel für eine Uhr oder einen anderen kleinen Bildschirm entwickeln und Hilfe brauchen, <a href="https://smallwater.eu/de/#kontakt">schreiben Sie uns</a>. Wie wir in einer anderen unserer Apps Rätsel erzeugen, steht im Beitrag über unseren <a href="https://smallwater.eu/de/blog/sudoku-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/shmup-apple-watch-devlog/" data-ga-destination="/de/blog/sudoku-generator/" data-ga-placement="inline">Sudoku-Generator</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>So generieren Sie ein Sudoku mit eindeutiger Lösung</title>
      <link>https://smallwater.eu/de/blog/sudoku-generator/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/sudoku-generator/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Kreuzworträtsel &amp; Puzzles</category>
      <description>Anleitung für einen Sudoku-Generator: schneller Solver mit Bitmasken, zufälliges volles Gitter, Felder leeren mit Eindeutigkeitsprüfung, Schwierigkeit.</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="Halb gelöstes Sudoku mit kleinen Kandidatenziffern und einem markierten Feld mit nur einem Kandidaten" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#warum-genau-eine-losung" data-ga="blog_toc_clicked" data-ga-heading-id="warum-genau-eine-losung">Warum genau eine Lösung</a></li><li><a href="#schritt-1-einen-schnellen-solver-schreiben" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-1-einen-schnellen-solver-schreiben">Schritt 1: Einen schnellen Solver schreiben</a></li><li><a href="#schritt-2-das-ganze-gitter-fullen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-2-das-ganze-gitter-fullen">Schritt 2: Das ganze Gitter füllen</a></li><li><a href="#warum-zufalliges-leeren-nicht-reicht" data-ga="blog_toc_clicked" data-ga-heading-id="warum-zufalliges-leeren-nicht-reicht">Warum zufälliges Leeren nicht reicht</a></li><li><a href="#schritt-3-felder-mit-eindeutigkeitsprufung-leeren" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-3-felder-mit-eindeutigkeitsprufung-leeren">Schritt 3: Felder mit Eindeutigkeitsprüfung leeren</a></li><li><a href="#schritt-4-schwierigkeit-uber-die-zahl-leerer-felder-einstellen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-4-schwierigkeit-uber-die-zahl-leerer-felder-einstellen">Schritt 4: Schwierigkeit über die Zahl leerer Felder einstellen</a></li><li><a href="#geschwindigkeit-und-zuverlassigkeit-des-generators" data-ga="blog_toc_clicked" data-ga-heading-id="geschwindigkeit-und-zuverlassigkeit-des-generators">Geschwindigkeit und Zuverlässigkeit des Generators</a></li><li><a href="#wo-die-zahl-leerer-felder-nicht-ausreicht" data-ga="blog_toc_clicked" data-ga-heading-id="wo-die-zahl-leerer-felder-nicht-ausreicht">Wo die Zahl leerer Felder nicht ausreicht</a></li></ol></nav><p>Ein Sudoku mit eindeutiger Lösung generieren Sie in zwei Schritten. Füllen Sie zuerst ein leeres 9×9-Gitter vollständig, per Backtracking mit Ziffern in zufälliger Reihenfolge. Leeren Sie dann einzelne Felder in zufälliger Reihenfolge und prüfen Sie nach jedem Leeren mit einem Solver, ob das Rätsel noch genau eine Lösung hat. Wenn nicht, setzen Sie die Ziffer zurück. Auf einer gewöhnlichen CPU dauert das im Median weniger als eine Millisekunde.</p>
<p>Das Verfahren passt zu jedem Spiel und jeder App, die beliebig viele neue Rätsel ohne Handarbeit braucht. Ein Beispiel aus der Praxis sind die Sudokus in unserer App mit Kreuzworträtseln und Denkspielen (<a href="https://smallwater.eu/crosswords-ios">App Store</a>, <a href="https://smallwater.eu/crosswords-android">Google Play</a>).</p>
<h2 id="warum-genau-eine-losung"><a class="heading-anchor" href="#warum-genau-eine-losung" aria-hidden="true" tabindex="-1">#</a>Warum genau eine Lösung</h2>
<p>Die Regeln kennt jeder: In jeder Zeile, jeder Spalte und jedem 3×3-Block steht jede Ziffer von 1 bis 9 genau einmal. Seltener wird erwähnt, dass ein gutes Rätsel <strong>nur eine</strong> Lösung hat. Nur dann lässt es sich mit reiner Logik lösen, ohne Raten.</p>
<p>Das hat auch eine praktische Seite. Ein Spiel speichert meist eine Lösung, prüft damit das Ergebnis und gibt Hinweise. Hat ein Rätsel zwei Lösungen, kann der Spieler bei der anderen landen, die genauso richtig ist, und das Spiel erkennt sie nicht an.</p>
<h2 id="schritt-1-einen-schnellen-solver-schreiben"><a class="heading-anchor" href="#schritt-1-einen-schnellen-solver-schreiben" aria-hidden="true" tabindex="-1">#</a>Schritt 1: Einen schnellen Solver schreiben</h2>
<p>Ein Solver ist ein Programm, das das Sudoku löst. Er arbeitet mit Backtracking: Er setzt eine der möglichen Ziffern in ein Feld, geht zum nächsten Feld weiter und kehrt bei einem Widerspruch zurück, um eine andere Ziffer zu probieren. Sie brauchen ihn zweimal: zum Füllen des leeren Gitters und zur Eindeutigkeitsprüfung. Die Prüfung läuft nach jedem geleerten Feld, also 40- bis 70-mal pro Rätsel, und muss deshalb schnell sein. Entscheidend sind zwei Dinge:</p>
<ul>
<li><strong>Bitmasken.</strong> Speichern Sie für jede Zeile, jede Spalte und jeden Block eine 9-Bit-Zahl mit den bereits verwendeten Ziffern. Die Kandidaten eines Feldes, also die Ziffern, die dort noch stehen dürfen, sind dann eine einzige Operation: alle Ziffern außer denen, die schon in seiner Zeile, Spalte oder seinem Block stehen. Die Anzahl der Kandidaten ist die Zahl der gesetzten Bits und lässt sich vorab in eine Tabelle mit 512 Werten rechnen.</li>
<li><strong>Zuerst das Feld mit den wenigsten Kandidaten.</strong> Nehmen Sie nicht das erste leere Feld von links, sondern das mit den wenigsten Möglichkeiten (in der Literatur die MRV-Heuristik, minimum remaining values). Ein Feld mit nur einem Kandidaten füllen Sie sofort, ein Feld ohne Kandidaten bedeutet einen toten Zweig, und Sie gehen zurück.</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="Halb gelöstes Gitter mit kleinen Kandidatenziffern in den leeren Feldern, ein Feld mit nur einem Kandidaten ist markiert" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Kandidaten in den leeren Feldern. Das markierte Feld hat nur einen Kandidaten, der Solver füllt es zuerst.</figcaption></figure>
<p>Bei schweren Rätseln braucht die Eindeutigkeitsprüfung mit dieser Regel im Median 311 Schritte. Nimmt der Solver die Felder der Reihe nach von links nach rechts, sind es im Median 28.579 Schritte und in den schlechtesten 5 % der Fälle über 300.000. Der Unterschied ist etwa 90-fach.</p>
<h2 id="schritt-2-das-ganze-gitter-fullen"><a class="heading-anchor" href="#schritt-2-das-ganze-gitter-fullen" aria-hidden="true" tabindex="-1">#</a>Schritt 2: Das ganze Gitter füllen</h2>
<p>Ein volles Gitter erhalten Sie mit demselben Solver auf einem leeren Gitter. Der einzige Unterschied: Die Kandidaten eines Feldes probieren Sie in zufälliger Reihenfolge. Die erste gefundene Lösung ist ein zufälliges, gültiges, vollständig gefülltes Gitter.</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="Vollständig gefülltes 9×9-Sudoku-Gitter" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Ein zufälliges volles Gitter. Daraus entsteht das Rätsel.</figcaption></figure>
<h2 id="warum-zufalliges-leeren-nicht-reicht"><a class="heading-anchor" href="#warum-zufalliges-leeren-nicht-reicht" aria-hidden="true" tabindex="-1">#</a>Warum zufälliges Leeren nicht reicht</h2>
<p>Die einfachste Idee ist, N zufällige Felder des vollen Gitters zu leeren, etwa 40, 50 oder 55 je nach Schwierigkeit. Das Rätsel sieht gut aus, hat aber oft keine eindeutige Lösung.</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="Sudoku-Rätsel, in dem vier Felder an den Ecken eines Rechtecks zwei mögliche Ziffernpaare haben" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Ein Rätsel aus zufälligem Leeren. In den markierten Feldern passen beide Ziffernpaare, das Rätsel hat zwei Lösungen.</figcaption></figure>
<p>Das Bild zeigt den typischen Fall. Vier Felder an den Ecken eines Rechtecks, das zwei Blöcke berührt, lassen sich auf zwei Arten füllen, und der Rest des Gitters entscheidet nicht darüber. Sudoku-Löser kennen dieses Muster als Deadly Pattern. Bei 1.000 Rätseln pro Schwierigkeit sieht das Ergebnis so aus:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Schwierigkeit</th>
<th align="right">Geleerte Felder</th>
<th align="right">Rätsel mit mehr als einer Lösung</th>
</tr>
</thead>
<tbody><tr>
<td>Leicht</td>
<td align="right">40</td>
<td align="right">59 %</td>
</tr>
<tr>
<td>Mittel</td>
<td align="right">50</td>
<td align="right">96 %</td>
</tr>
<tr>
<td>Schwer</td>
<td align="right">55</td>
<td align="right">100 %</td>
</tr>
</tbody></table>
</div>
<p>Ein schweres Rätsel aus zufälligem Leeren hat im Schnitt Hunderte Lösungen. Eine Eindeutigkeitsprüfung nach jedem Leeren ist deshalb nötig.</p>
<h2 id="schritt-3-felder-mit-eindeutigkeitsprufung-leeren"><a class="heading-anchor" href="#schritt-3-felder-mit-eindeutigkeitsprufung-leeren" aria-hidden="true" tabindex="-1">#</a>Schritt 3: Felder mit Eindeutigkeitsprüfung leeren</h2>
<p>Gehen Sie alle 81 Felder in zufälliger Reihenfolge durch. Versuchen Sie, jedes zu leeren, und zählen Sie mit dem Solver die Lösungen, brechen Sie aber bei zwei ab. Mehr müssen Sie nicht wissen: Eine Lösung heißt, das Feld darf leer bleiben, zwei heißen, es muss zurück.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">rätsel = Kopie des vollen Gitters
geleert = 0
für jedes Feld in zufälliger Reihenfolge:
    wenn geleert == ziel: Ende
    ziffer = rätsel[feld]
    rätsel[feld] = leer
    wenn anzahl_lösungen(rätsel, höchstens 2) == 1:
        geleert = geleert + 1
    sonst:
        rätsel[feld] = ziffer</code></pre>
<p>Ein Feld, das Sie einmal zurückgesetzt haben, probieren Sie nicht noch einmal. Weiteres Leeren lockert das Rätsel nur, ein Feld, das sich jetzt nicht leeren lässt, lässt sich also auch später nicht leeren.</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="Halb fertiges schweres Rätsel mit 37 leeren Feldern und einer markierten Ziffer, die zurückgesetzt werden musste" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Nach 37 geleerten Feldern. Die markierte Ziffer ließ sich nicht entfernen, das Rätsel hätte zwei Lösungen, deshalb kam sie zurück.</figcaption></figure>
<h2 id="schritt-4-schwierigkeit-uber-die-zahl-leerer-felder-einstellen"><a class="heading-anchor" href="#schritt-4-schwierigkeit-uber-die-zahl-leerer-felder-einstellen" aria-hidden="true" tabindex="-1">#</a>Schritt 4: Schwierigkeit über die Zahl leerer Felder einstellen</h2>
<p>Das einfachste Maß für die Schwierigkeit ist die Zahl der geleerten Felder. Die drei Stufen, die wir in unserem Spiel verwenden:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Schwierigkeit</th>
<th align="right">Leere Felder</th>
<th align="right">Verbleibende Ziffern</th>
<th align="right">Im Schnitt zurückgesetzte Felder</th>
</tr>
</thead>
<tbody><tr>
<td>Leicht</td>
<td align="right">40</td>
<td align="right">41</td>
<td align="right">0,9</td>
</tr>
<tr>
<td>Mittel</td>
<td align="right">50</td>
<td align="right">31</td>
<td align="right">4,5</td>
</tr>
<tr>
<td>Schwer</td>
<td align="right">55</td>
<td align="right">26</td>
<td align="right">12,7</td>
</tr>
</tbody></table>
</div>
<p>Je weniger Ziffern übrig sind, desto öfter würde ein weiteres Leeren die Eindeutigkeit zerstören, und das Feld muss zurück. Auf der schweren Stufe bleibt ein einzelner Durchlauf manchmal stehen, bevor er 55 leere Felder erreicht. Generieren Sie dann ein neues volles Gitter und versuchen Sie es erneut. Bei einer Messung an 1.000 schweren Rätseln reichte der erste Durchlauf in 97,6 % der Fälle, und es waren nie mehr als 3 Versuche nötig.</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="Fertiges schweres Sudoku-Rätsel mit 26 Ziffern" width="1024" height="1034" loading="lazy" decoding="async" /></picture><figcaption>Ein fertiges schweres Rätsel: 26 Ziffern und eine einzige Lösung.</figcaption></figure>
<h2 id="geschwindigkeit-und-zuverlassigkeit-des-generators"><a class="heading-anchor" href="#geschwindigkeit-und-zuverlassigkeit-des-generators" aria-hidden="true" tabindex="-1">#</a>Geschwindigkeit und Zuverlässigkeit des Generators</h2>
<p>Wir haben den Generator an 1.000 Rätseln pro Schwierigkeit laufen lassen (Node.js 22 auf einer Server-CPU, ein Telefon ist langsamer):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Schwierigkeit</th>
<th align="right">Median</th>
<th align="right">95 % der Rätsel unter</th>
<th align="right">Längste Zeit</th>
<th align="right">Rätsel mit mehreren Lösungen</th>
</tr>
</thead>
<tbody><tr>
<td>Leicht</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>Mittel</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>Schwer</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>Zusätzlich haben wir jedes generierte Rätsel unabhängig mit dem Solver geprüft. Alle 3.000 hatten eine einzige Lösung.</p>
<h2 id="wo-die-zahl-leerer-felder-nicht-ausreicht"><a class="heading-anchor" href="#wo-die-zahl-leerer-felder-nicht-ausreicht" aria-hidden="true" tabindex="-1">#</a>Wo die Zahl leerer Felder nicht ausreicht</h2>
<p>Die Zahl der vorgegebenen Ziffern ist ein grobes Maß für die Schwierigkeit. Zwei Rätsel mit 26 Ziffern können unterschiedlich schwer sein, je nachdem, welche Lösungstechniken sie verlangen. Eine genauere Bewertung würde das Rätsel wie ein Mensch lösen und die schwierigste Technik festhalten, die nötig war. Für ein Handyspiel, in dem der Spieler aus drei Stufen wählt, genügt die Zahl der vorgegebenen Ziffern.</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">Ausprobieren</p><h2 class="app-box-title">Kreuzworträtsel &amp; Puzzles</h2><p>Kreuzworträtsel, Wortsuche, Sudoku und Nonogramme in einer Offline-App. Wie die Rätsel entstehen und was neu ist.</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>Wenn Sie ein ähnliches Problem lösen, Rätsel generieren oder eine mobile App brauchen, <a href="https://smallwater.eu/de/#kontakt">schreiben Sie uns</a>. Wie man ein Wortsuchrätsel mit Lösungswort generiert, beschreibt eine <a href="https://smallwater.eu/de/blog/word-search-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/sudoku-generator/" data-ga-destination="/de/blog/word-search-generator/" data-ga-placement="inline">eigene Anleitung</a>. Dem Schwedenrätsel mit Lösungswort widmet sich eine <a href="https://smallwater.eu/de/blog/swedish-crossword-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/sudoku-generator/" data-ga-destination="/de/blog/swedish-crossword-generator/" data-ga-placement="inline">weitere Anleitung</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Wie man ein Schwedenrätsel mit Lösungswort generiert</title>
      <link>https://smallwater.eu/de/blog/swedish-crossword-generator/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/swedish-crossword-generator/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Kreuzworträtsel &amp; Puzzles</category>
      <description>Anleitung für einen Schwedenrätsel-Generator: Maske per Hill Climbing, Füllen als CSP mit Backtracking, das Lösungswort erst im fertigen Gitter.</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="Zwei Schwedenrätsel 10×10: links eine zufällige Maske mit bestraften Feldern, rechts ein gefülltes Gitter mit den Buchstaben des Lösungsworts in Koralle" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#was-ein-schwedenratsel-erfullen-muss" data-ga="blog_toc_clicked" data-ga-heading-id="was-ein-schwedenratsel-erfullen-muss">Was ein Schwedenrätsel erfüllen muss</a></li><li><a href="#warum-wort-fur-wort-fullen-nicht-reicht" data-ga="blog_toc_clicked" data-ga-heading-id="warum-wort-fur-wort-fullen-nicht-reicht">Warum Wort für Wort füllen nicht reicht</a></li><li><a href="#schritt-1-die-maske-per-hill-climbing" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-1-die-maske-per-hill-climbing">Schritt 1: die Maske per Hill Climbing</a></li><li><a href="#schritt-2-fullen-mit-wortern-als-csp" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-2-fullen-mit-wortern-als-csp">Schritt 2: Füllen mit Wörtern als CSP</a></li><li><a href="#schritt-3-das-losungswort-im-fertigen-gitter" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-3-das-losungswort-im-fertigen-gitter">Schritt 3: das Lösungswort im fertigen Gitter</a></li><li><a href="#warum-das-losungswort-nicht-vorab-wie-in-der-zeitung" data-ga="blog_toc_clicked" data-ga-heading-id="warum-das-losungswort-nicht-vorab-wie-in-der-zeitung">Warum das Lösungswort nicht vorab wie in der Zeitung</a></li><li><a href="#leistung-des-generators-und-warum-er-nicht-auf-dem-handy-lauft" data-ga="blog_toc_clicked" data-ga-heading-id="leistung-des-generators-und-warum-er-nicht-auf-dem-handy-lauft">Leistung des Generators und warum er nicht auf dem Handy läuft</a></li><li><a href="#generieren-in-gro-en-mengen" data-ga="blog_toc_clicked" data-ga-heading-id="generieren-in-gro-en-mengen">Generieren in großen Mengen</a></li><li><a href="#was-der-ansatz-nicht-abdeckt" data-ga="blog_toc_clicked" data-ga-heading-id="was-der-ansatz-nicht-abdeckt">Was der Ansatz nicht abdeckt</a></li></ol></nav><p>Ein Schwedenrätsel mit Lösungswort generieren Sie in zwei Phasen: Suchen Sie zuerst mit einer lokalen Suche die Maske, also die Verteilung von Fragefeldern und Buchstabenfeldern, und füllen Sie sie dann mit Wörtern aus dem Wörterbuch als Constraint-Satisfaction-Problem (CSP) mit Backtracking. Setzen Sie das Lösungswort nicht vorab ins Gitter. Erst wenn das Rätsel fertig ist, suchen Sie einen Witz, dessen Pointe sich darin von links nach rechts und von oben nach unten lesen lässt.</p>
<p>So entstehen die Schwedenrätsel in unserer App mit Kreuzworträtseln und Wortsuchrätseln (<a href="https://smallwater.eu/crosswords-ios">App Store</a>, <a href="https://smallwater.eu/crosswords-android">Google Play</a>). Ein Gitter kostet wenige Sekunden bis einige Dutzend Sekunden Rechenzeit, deshalb generieren wir die Rätsel vorab auf einem leistungsfähigeren Rechner und liefern sie mit der App aus. Wortsuchrätsel entstehen ganz anders und einfacher, das beschreibt der Artikel <a href="https://smallwater.eu/de/blog/word-search-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/swedish-crossword-generator/" data-ga-destination="/de/blog/word-search-generator/" data-ga-placement="inline">Wie man ein Wortsuchrätsel mit Lösungswort generiert</a>.</p>
<h2 id="was-ein-schwedenratsel-erfullen-muss"><a class="heading-anchor" href="#was-ein-schwedenratsel-erfullen-muss" aria-hidden="true" tabindex="-1">#</a>Was ein Schwedenrätsel erfüllen muss</h2>
<p>Ein Schwedenrätsel hat keine schwarzen Felder. Jedes Feld ist ein Buchstabe oder ein Fragefeld mit Pfeil nach rechts oder nach unten. Daraus ergeben sich harte Bedingungen:</p>
<ul>
<li>Jeder Buchstabe gehört zu einem waagerechten und einem senkrechten Wort. Ein Buchstabe, den nur ein Wort kreuzt, lässt sich nicht überprüfen und wirkt wie ein Fehler.</li>
<li>Jedes Wort hat mindestens zwei Buchstaben und davor ein Fragefeld. Deshalb bestehen die erste Zeile und die erste Spalte immer nur aus Fragefeldern.</li>
<li>Die Buchstaben bilden eine zusammenhängende Fläche ohne abgetrennte Inseln.</li>
<li>Jedes Wort muss im Wörterbuch stehen und mindestens eine Umschreibung haben. Kein Wort kommt im Gitter zweimal vor.</li>
</ul>
<p>Dazu kommen weiche Qualitätskriterien: wenige Wörter mit zwei Buchstaben, keine Fragefelder, die ins Leere zeigen, und möglichst wenige sehr lange Wörter, die schwer zu füllen sind. Das Problem ist rechnerisch schwer. <a href="https://link.springer.com/chapter/10.1007/978-3-642-30347-0_15" target="_blank" rel="noopener">Engel et al. (2012)</a> haben gezeigt, dass schon das Füllen einer Maske mit Wörtern NP-vollständig ist.</p>
<h2 id="warum-wort-fur-wort-fullen-nicht-reicht"><a class="heading-anchor" href="#warum-wort-fur-wort-fullen-nicht-reicht" aria-hidden="true" tabindex="-1">#</a>Warum Wort für Wort füllen nicht reicht</h2>
<p>Der erste Gedanke ist, wie ein Mensch mit Bleistift vorzugehen: das Lösungswort in Wörter teilen, sie wie in der Zeitung in eine Zeile legen, durch jeden Buchstaben ein kreuzendes Wort führen und den Rest des Gitters mit einem Greedy Algorithm auffüllen. Ein Greedy Algorithm wählt in jedem Schritt, was gerade am besten aussieht, und revidiert die Entscheidung nie. Wo nichts passt, bleibt ein Loch. Und weil Greedy-Füllen nicht zurückgehen kann, rettet der Rest des Gitters nichts mehr, wenn die ersten zehn Wörter schlecht sind.</p>
<p>Eine bessere Heuristik für die Wortwahl hilft nur wenig. Sie können die Längen der kreuzenden Wörter von mittleren, um die fünf Buchstaben, hin zu den Extremen probieren. Oder evolutionär vorgehen: eine Population halbfertiger Rätsel, aus jedem fünf Nachkommen mit je einem Wort mehr, bewertet nach gefüllten Feldern, Anzahl der Vokale und Gleichmäßigkeit der Buchstaben, und nur die besten kommen in die nächste Generation. Die Ergebnisse werden etwas dichter, aber die Löcher bleiben, weil die Heuristik nur den nächsten Schritt sieht.</p>
<p>Den Ausweg zeigt die Bachelorarbeit von Jakob Engel an der TU München (2009); er ist auch Erstautor des oben genannten Artikels. Teilen Sie die Aufgabe in zwei unabhängige Phasen: Finden Sie erst eine gute Maske, ohne auf Wörter zu achten, und füllen Sie sie dann. Engel verwendete für die Maske einen evolutionären Algorithmus. Wir verwenden eine einfachere lokale Suche mit zufälligen Erschütterungen, beschrieben im nächsten Schritt.</p>
<h2 id="schritt-1-die-maske-per-hill-climbing"><a class="heading-anchor" href="#schritt-1-die-maske-per-hill-climbing" aria-hidden="true" tabindex="-1">#</a>Schritt 1: die Maske per Hill Climbing</h2>
<p>Die Maske ist ein Gitter, in dem jedes Feld entweder Fragefeld oder Buchstabe ist. Um sie zu finden, reicht Hill Climbing: eine lokale Suche, die kleine Änderungen macht und diejenigen behält, die das Ergebnis nicht verschlechtern.</p>
<p>Beginnen Sie zufällig, mit Fragefeldern auf etwa 22 % der inneren Felder. Geben Sie jeder Maske eine Strafpunktzahl. Harte Verstöße (zu kurzes Wort, Buchstabe ohne kreuzendes Wort, abgetrennte Insel) sollten meist 1.000 Punkte kosten, weiche Mängel (Zweibuchstabenwörter über dem Budget, ein Fragefeld, das auf kein Wort zeigt, gehäufte Fragefelder) 50 bis 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="Eine zufällige 10×10-Maske mit vielen rot und orange markierten Problemfeldern" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Zufällige Maske. Rote Felder sind Buchstaben mit einem Verstoß, orange sind Fragefelder, die ins Leere zeigen. Strafe 35.500.</figcaption></figure>
<p>Ändern Sie dann in einer Schleife ein Feld von Fragefeld zu Buchstabe oder umgekehrt, oder verschieben Sie ein Fragefeld um ein Feld diagonal. Behalten Sie die Änderung, wenn die Strafe nicht gestiegen ist, sonst machen Sie sie rückgängig. Richten Sie die Hälfte der Änderungen auf die Felder mit der höchsten Strafe, dann verschwinden grobe Fehler schnell:</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="Dieselbe Maske nach 150 Schritten, nur noch wenige Problemfelder" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Nach 150 Schritten. Strafe 1.770.</figcaption></figure>
<p>Hill Climbing bleibt leicht in einem lokalen Minimum hängen. Wenn sich 2.000 Schritte lang nichts verbessert, „schütteln“ Sie das Gitter: Schalten Sie in der besten bisher gefundenen Maske fünf zufällige Felder um und suchen Sie weiter. Das sind die Spitzen im Diagramm. Bringen auch drei Erschütterungen keine bessere Maske und hat sie keine harten Verstöße, hören Sie auf.</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="Strafkurve in logarithmischer Skala über 8.300 Schritte: schneller Abfall von 35.500 auf 200 und drei kurze Spitzen nach dem Schütteln" width="1800" height="850" loading="lazy" decoding="async" /></picture><figcaption>Strafe während der Generierung einer 10×10-Maske. Die Punkte markieren die beiden vorherigen Momentaufnahmen.</figcaption></figure>
<p>Die fertige Maske hat eine Reststrafe von 200 für weiche Mängel, aber keinen harten Verstoß:</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="Fertige 10×10-Maske mit Pfeilen in den Fragefeldern und leeren Buchstabenfeldern" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Die fertige Maske. Die Pfeile zeigen die Richtung der Wörter.</figcaption></figure>
<p>Die Strafen für ein Fragefeld ohne Wort und für eine abgetrennte Insel sollten von Anfang an in der Maske stecken. In einem großen Bestand fertiger Rätsel ohne sie zeigten 13,2 % der Fragefelder auf kein Wort, und 23,5 % der Gitter hatten eine abgetrennte Buchstabeninsel. Mit ihnen sind es 5,0 % und 0 %.</p>
<h2 id="schritt-2-fullen-mit-wortern-als-csp"><a class="heading-anchor" href="#schritt-2-fullen-mit-wortern-als-csp" aria-hidden="true" tabindex="-1">#</a>Schritt 2: Füllen mit Wörtern als CSP</h2>
<p>Aus der Maske entsteht eine Liste von Slots, also Plätzen für Wörter mit bekannter Länge und bekannten Kreuzungen. Das Füllen ist ein klassisches Constraint-Satisfaction-Problem (CSP): Jeder Slot ist eine Variable, seine Domäne sind die Wörter, die hineinpassen, und die Bedingungen sind gleiche Buchstaben an den Kreuzungen. Gelöst wird es mit Backtracking, einer Suche, die in einem toten Zweig einen Schritt zurückgeht und eine andere Möglichkeit probiert:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">für jeden Slot: Domäne = alle Wörter dieser Länge
solange nicht alle Slots gefüllt sind:
    wähle den Slot mit der kleinsten Domäne (bei Gleichstand den mit den meisten Kreuzungen)
    für jedes Wort der Domäne, das beste zuerst:
        setze das Wort
        schränke die Domänen der kreuzenden Slots auf Wörter mit passendem Buchstaben ein
        wenn eine Domäne leer wurde: rückgängig machen, nächstes Wort versuchen
        sonst mit dem nächsten Slot weitermachen
    wenn kein Wort passt: einen Slot zurückgehen
nach 50.000 Schritten aufgeben</code></pre>
<p>In der Literatur heißen diese Heuristiken MRV (Minimum Remaining Values, zuerst der Slot mit den wenigsten Möglichkeiten) und Forward Checking (nach jedem Setzen die Domänen der Nachbarn einschränken, damit ein toter Zweig sofort auffällt und nicht erst zehn Wörter später). Indizieren Sie das Wörterbuch vorab nach Länge, Position und Buchstabe. „Wörter mit Q an dritter Stelle“ sind dann eine einzige Schnittmenge, kein Durchlauf durch das ganze Wörterbuch.</p>
<p>Auch die Reihenfolge der Wörter zählt. Geben Sie Wörtern mit mehr verschiedenen Umschreibungen im Wörterbuch den Vorrang. Das ist ein einfaches Signal für ein gängiges Rätselwort. Ohne das füllt sich das Gitter mit unbekannten Abkürzungen.</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="Eine 10×10-Maske, gefüllt mit englischen Wörtern" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Das gefüllte Gitter. Die Abbildungen verwenden das englische Wörterbuch, die Zahlen in den Tabellen stammen aus dem slowakischen.</figcaption></figure>
<h2 id="schritt-3-das-losungswort-im-fertigen-gitter"><a class="heading-anchor" href="#schritt-3-das-losungswort-im-fertigen-gitter" aria-hidden="true" tabindex="-1">#</a>Schritt 3: das Lösungswort im fertigen Gitter</h2>
<p>Beim Füllen wissen Sie noch nicht, wie das Lösungswort lautet. Lesen Sie die Buchstaben des fertigen Gitters von links nach rechts und von oben nach unten und suchen Sie einen Witz, dessen Pointe in dieser Zeichenkette als Teilfolge vorkommt. Die Buchstaben stehen also in der richtigen Reihenfolge, müssen aber nicht nebeneinander liegen. Wird einer gefunden, verteilen Sie die Buchstaben möglichst gleichmäßig über das Gitter, damit sich die Pointe nicht aus einem einzigen Wort erraten lässt.</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="Das gefüllte Gitter mit zehn Buchstaben S, P, A, C, E, S, U, I, T, S in Koralle, in Lesereihenfolge nummeriert" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Das Lösungswort SPACESUITS in Lesereihenfolge. In dieses Gitter würden 111 verschiedene Pointen aus unserer englischen Witzliste passen.</figcaption></figure>
<p>Diese Reihenfolge (erst das Gitter, dann der Witz) erhöht die Zahl fertiger Rätsel drastisch. In unserer Messung passt ein einziges 12×12-Gitter im Schnitt zu 86 Witzen aus der slowakischen Liste (5.960 Witze). Es findet sich also immer einer, der in dieser Größe noch nicht verwendet wurde.</p>
<h2 id="warum-das-losungswort-nicht-vorab-wie-in-der-zeitung"><a class="heading-anchor" href="#warum-das-losungswort-nicht-vorab-wie-in-der-zeitung" aria-hidden="true" tabindex="-1">#</a>Warum das Lösungswort nicht vorab wie in der Zeitung</h2>
<p>In der Zeitung steht das Lösungswort meist an einer Stelle, in einer Zeile und in Wörter geteilt. Dahin führen zwei Wege, und beide liefern sehr wenige Ergebnisse.</p>
<p>Der erste Weg ist, das Lösungswort vorab ins Gitter zu setzen. Dann muss es hineinpassen. Unsere Pointen haben im Schnitt 10,3 Buchstaben und 2 Wörter. So groß ist der Anteil der Witze aus der slowakischen Liste, die sich in einer bestimmten Größe überhaupt platzieren lassen:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Gitter</th>
<th align="right">Ganze Pointe in einer Zeile</th>
<th align="right">Pointe in Wörter geteilt (höchstens 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="Ein 10×10-Gitter, in dem die Wörter des Lösungsworts KNOW und RULES vorab als ganze waagerechte Wörter in Koralle platziert sind" width="968" height="978" loading="lazy" decoding="async" /></picture><figcaption>Das Lösungswort vorab als ganze Wörter in Zeilen gesetzt, wie in der Zeitung. Erzeugt mit demselben Generator.</figcaption></figure>
<p>Mit vorab gesetztem Lösungswort gelingt das Füllen kleiner Gitter etwa so oft wie ohne. Bei 15×15 waren es 5 von 60 Versuchen gegenüber 8 ohne, bei 18×18 keiner von 60. Jedes solche Gitter dient außerdem nur einem Witz, und in kleinen Größen lässt sich die Mehrheit der Witze gar nicht platzieren.</p>
<p>Der zweite Weg ist, das Gitter frei entstehen zu lassen und die Pointe darin als zusammenhängenden Abschnitt zu suchen. In einer Stichprobe von 2.200 fertigen slowakischen Rätseln (200 jeder Größe von 8×8 bis 18×18) fand sich die ganze Pointe als zusammenhängender Abschnitt in einer Zeile oder Spalte nur in 81 Gittern (3,7 %). Darf jedes Wort der Pointe an einer anderen Stelle liegen, sind es 443 Gitter (20 %), und das noch ohne die Bedingung, dass die Wörter nebeneinander stehen. In Lesereihenfolge fand sich in jedem von ihnen ein Witz. Für den Preis eines über das Gitter verstreuten Lösungsworts bekommen Sie um Größenordnungen mehr Rätsel.</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">Ausprobieren</p><h2 class="app-box-title">Kreuzworträtsel &amp; Puzzles</h2><p>Kreuzworträtsel, Wortsuche, Sudoku und Nonogramme in einer Offline-App. Wie die Rätsel entstehen und was neu ist.</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="leistung-des-generators-und-warum-er-nicht-auf-dem-handy-lauft"><a class="heading-anchor" href="#leistung-des-generators-und-warum-er-nicht-auf-dem-handy-lauft" aria-hidden="true" tabindex="-1">#</a>Leistung des Generators und warum er nicht auf dem Handy läuft</h2>
<p>Der Generator lief mit dem slowakischen Wörterbuch 60 Versuche pro Größe, in einem Thread (JavaScript auf Node 22, Server-CPU Intel Xeon 2,8 GHz in der Cloud). Ein Versuch ist eine Maske und ein Füllvorgang. „Erfolgreich gefüllt“ heißt, dass der CSP innerhalb von 50.000 Schritten eine Lösung gefunden hat.</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Gitter</th>
<th align="right">Maske</th>
<th align="right">Füllen</th>
<th align="right">Erfolgreich gefüllt</th>
<th align="right">Wörter im Gitter</th>
<th align="right">Witze pro Gitter</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 (aus 3 Gittern)</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>Was die Messung zeigt:</p>
<ul>
<li><strong>Ein kleines Gitter ist kein leichtes.</strong> Ein 8×8-Gitter hat viele kurze Slots mit vielen Kreuzungen und wenig Spielraum, das Füllen gelingt nur in 5 % der Versuche. Ein gefülltes Gitter kostet im Schnitt etwa 10 Sekunden Rechenzeit bei 8×8, 4 Sekunden bei 12×12 und 18 Sekunden bei 18×18.</li>
<li><strong>Die meisten Versuche scheitern.</strong> Starten Sie Versuche deshalb parallel auf allen Kernen und nehmen Sie das erste ausreichend gute Ergebnis.</li>
<li><strong>Auf dem Handy ergibt das keinen Sinn.</strong> Niemand will beim Öffnen eines Rätsels Dutzende Sekunden warten, und die App müsste das ganze Wörterbuch mit Umschreibungen mitliefern. Ein Generator in der App muss außerdem ständig die Kontrolle an die Oberfläche abgeben, sonst friert die App ein.</li>
</ul>
<h2 id="generieren-in-gro-en-mengen"><a class="heading-anchor" href="#generieren-in-gro-en-mengen" aria-hidden="true" tabindex="-1">#</a>Generieren in großen Mengen</h2>
<p>Der Generator gehört deshalb auf einen Rechner mit Mehrkernprozessor. Bei uns hat jeder Kern einen eigenen Thread mit dem Wörterbuch, das einmal beim Start geladen wird. Ein Gitter ist fertig, sobald einer der Threads ein Ergebnis mit ausreichend wenigen ungenutzten Feldern liefert. Im Dauerbetrieb erzeugt er reihum ein Rätsel jeder Größe in jeder Sprache. So sind über 14.000 Rätsel in vier Sprachen (Slowakisch, Tschechisch, Englisch, Deutsch) und elf Größen von 8×8 bis 18×18 entstanden. Jeder Witz kommt pro Größe und Sprache höchstens einmal vor.</p>
<p>Bei dieser Menge zählen zwei Dinge:</p>
<ul>
<li><strong>Format.</strong> Ein vollständiger Datensatz braucht etwa 28 kB pro Rätsel und 390 MB für den ganzen Bestand. Das meiste lässt sich berechnen (Koordinaten, Feldtypen, Nummern der Fragefelder). Ein kompaktes Format mit dem Gitter als einer einzigen Zeichenkette braucht etwa 2 kB, 15 MB für den Bestand und etwa 7 MB im Installationspaket der App.</li>
<li><strong>Qualität messen.</strong> Messen Sie nach jeder Generierung den Anteil der Fragefelder ohne Wort und der Gitter mit Inseln, und lassen Sie die Prüfung fehlschlagen, wenn die Zahlen schlechter werden.</li>
</ul>
<h2 id="was-der-ansatz-nicht-abdeckt"><a class="heading-anchor" href="#was-der-ansatz-nicht-abdeckt" aria-hidden="true" tabindex="-1">#</a>Was der Ansatz nicht abdeckt</h2>
<ul>
<li><strong>Fragefelder ohne Wort.</strong> Das durchschnittliche Rätsel hat immer noch 2,7 Fragefelder, die ins Leere zeigen. Die linke obere Ecke ist immer eines davon.</li>
<li><strong>Wörterbücher ohne Häufigkeiten.</strong> Das deutsche Wörterbuch aus Wiktionary hat für die meisten Wörter nur eine Umschreibung, die Sortierung nach Anzahl der Umschreibungen unterscheidet daher kaum, und seltene Wörter landen im Gitter. Eine Häufigkeitsliste würde helfen, wir haben noch keine.</li>
<li><strong>Das Lösungswort steht nicht an einer Stelle.</strong> Wer es aus der Zeitung kennt, sucht es vielleicht in einer Zeile. Das ist ein Kompromiss, kein Ideal.</li>
</ul>
<p>Wenn Sie ein ähnliches Problem mit generierten Inhalten lösen oder eine mobile App brauchen, <a href="https://smallwater.eu/de/#kontakt">schreiben Sie uns</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>So generieren Sie ein 3D-Handymodell im Browser</title>
      <link>https://smallwater.eu/de/blog/threejs-csg-3d-phone-model/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/threejs-csg-3d-phone-model/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Ecranify</category>
      <description>Anleitung für ein 3D-Handymodell im Browser mit Three.js und CSG: Körper mit Bevel, Ausschnitte für Tasten und Lautsprecher, Web Worker, schnelles Bohren.</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="Zwei generierte 3D-Handymodelle, eines von vorne mit dunklem Display, eines von hinten" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#was-sie-brauchen" data-ga="blog_toc_clicked" data-ga-heading-id="was-sie-brauchen">Was Sie brauchen</a></li><li><a href="#schritt-1-ein-rechteck-in-3d-extrudieren" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-1-ein-rechteck-in-3d-extrudieren">Schritt 1: Ein Rechteck in 3D extrudieren</a></li><li><a href="#schritt-2-eine-mulde-fur-das-display-ausschneiden" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-2-eine-mulde-fur-das-display-ausschneiden">Schritt 2: Eine Mulde für das Display ausschneiden</a></li><li><a href="#schritt-3-seitliche-elemente-als-bohrer-vorbereiten" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-3-seitliche-elemente-als-bohrer-vorbereiten">Schritt 3: Seitliche Elemente als Bohrer vorbereiten</a></li><li><a href="#schritt-4-details-auf-der-vorderseite-nicht-bohren" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-4-details-auf-der-vorderseite-nicht-bohren">Schritt 4: Details auf der Vorderseite nicht bohren</a></li><li><a href="#schritt-5-die-berechnung-in-einen-web-worker-verlagern" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-5-die-berechnung-in-einen-web-worker-verlagern">Schritt 5: Die Berechnung in einen Web Worker verlagern</a></li><li><a href="#schritt-6-alle-locher-auf-einmal-abziehen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-6-alle-locher-auf-einmal-abziehen">Schritt 6: Alle Löcher auf einmal abziehen</a></li><li><a href="#schritt-7-die-segmentzahl-als-parameter-fuhren" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-7-die-segmentzahl-als-parameter-fuhren">Schritt 7: Die Segmentzahl als Parameter führen</a></li><li><a href="#rendern-auf-einem-server-ohne-grafikkarte" data-ga="blog_toc_clicked" data-ga-heading-id="rendern-auf-einem-server-ohne-grafikkarte">Rendern auf einem Server ohne Grafikkarte</a></li></ol></nav><p>Ein 3D-Modell eines Smartphones generieren Sie im Browser, indem Sie ein Rechteck mit abgerundeten Ecken in die Tiefe extrudieren und daraus mit booleschen Operationen (CSG) Vorderseite, Tasten, Lautsprecher und Anschluss ausschneiden. Das ganze Gerät wird dann durch ein paar Zahlen in Millimetern beschrieben, und ein neues Modell bedeutet, diese Zahlen zu ändern, nicht in einem 3D-Programm zu modellieren.</p>
<p>Das lohnt sich überall dort, wo Sie viele ähnliche Geräte brauchen oder Nutzer sie anpassen sollen: Konfiguratoren, Mockups, Produktvorschauen. Ein Beispiel aus der Praxis ist <a href="https://ecranify.com" target="_blank" rel="noopener">Ecranify</a>, ein Tool für 3D-Mockups für Screenshots im App Store und bei Google Play, das kein einziges fertiges 3D-Modell enthält. Unten finden Sie den Ablauf Schritt für Schritt, samt einer Messung, die zeigt, wo sich bei der Berechnung am meisten sparen lässt.</p>
<h2 id="was-sie-brauchen"><a class="heading-anchor" href="#was-sie-brauchen" aria-hidden="true" tabindex="-1">#</a>Was Sie brauchen</h2>
<ul>
<li><a href="https://threejs.org" target="_blank" rel="noopener">Three.js</a> für Szene, Kameras, Lichter und vor allem Geometrie. Geometrie ist in Three.js ein Mesh, also eine Menge von Dreiecken.</li>
<li>Die Bibliothek <a href="https://github.com/gkjohnson/three-bvh-csg" target="_blank" rel="noopener">three-bvh-csg</a> für boolesche Operationen auf Meshes. Sie beschleunigt sie mit einem räumlichen Index (BVH, Bounding Volume Hierarchy) und testet so nicht jedes Dreieck gegen jedes andere.</li>
<li>Eine Beschreibung des Geräts als Daten, zum Beispiel JSON mit Gehäusemaßen, Eckenradius, Bevel-Größe und einer Liste der Elemente an den Seiten. Geben Sie alle Positionen in Millimetern ab der Gerätemitte an.</li>
</ul>
<h2 id="schritt-1-ein-rechteck-in-3d-extrudieren"><a class="heading-anchor" href="#schritt-1-ein-rechteck-in-3d-extrudieren" aria-hidden="true" tabindex="-1">#</a>Schritt 1: Ein Rechteck in 3D extrudieren</h2>
<p>Zeichnen Sie ein 2D-Rechteck mit abgerundeten Ecken in Originalgröße, zum Beispiel 163,4 × 78 mm, und extrudieren Sie es auf eine Tiefe von 8,75 mm. Sie erhalten eine Platte mit scharfen Kanten. So sieht kein Handy aus, deshalb schalten Sie beim Extrudieren auch einen Bevel ein, also eine abgerundete Kante. Three.js erledigt beides in einem Schritt.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Nur Extrusion</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="Kantige Platte ohne abgerundete Ecken und Kanten" width="1200" height="800" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">+ Ecken und 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="Dieselbe Platte mit abgerundeten Ecken und Kanten" width="1200" height="800" loading="lazy" decoding="async" /></picture></div></div><figcaption>Dieselbe Form, zwei andere Zahlen: Eckenradius und Bevel-Größe.</figcaption></figure>
<p>Achten Sie auf eines: Der Bevel wird nach außen hinzugefügt. Wenn Sie exakte Außenmaße brauchen, verkleinern Sie die 2D-Form und die Tiefe um die Bevel-Größe.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">b = min(Bevel, Tiefe / 2)
Form = abgerundetes Rechteck(Breite - 2b, Höhe - 2b, Eckenradius - b)
Körper = extrudiere(Form, Tiefe - 2b, Bevel b)</code></pre>
<p>Wie glatt die Rundungen sind (Tessellation), hängt von der Zahl der Segmente ab, in die ein Bogen zerlegt wird. Mehr Segmente bedeuten eine glattere Form und mehr Dreiecke. Diese Zahl entscheidet später über die Geschwindigkeit der ganzen Berechnung, deshalb kommen wir in Schritt 7 darauf zurück.</p>
<h2 id="schritt-2-eine-mulde-fur-das-display-ausschneiden"><a class="heading-anchor" href="#schritt-2-eine-mulde-fur-das-display-ausschneiden" aria-hidden="true" tabindex="-1">#</a>Schritt 2: Eine Mulde für das Display ausschneiden</h2>
<p>CSG (Constructive Solid Geometry) setzt Körper aus einfacheren Körpern mit drei Operationen zusammen: Vereinigung, Differenz und Schnitt. Die Differenz funktioniert wie ein Bohrer: Ein Körper nimmt Material aus einem anderen weg.</p>
<p>Das Display eines Handys liegt nicht bündig mit dem Rahmen, sondern in einer flachen Mulde. Dafür erstellen Sie eine zweite, um 1,4 mm kleinere Kopie des Körpers, behalten davon nur den vorderen, 0,7 mm dicken Teil (Schnitt mit einem großen Quader) und ziehen diesen Teil vom Körper ab. In die entstandene Mulde legen Sie dann eine dunkle Displayfläche.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Schneidkörper</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="Handykörper mit einem blauen, durchscheinenden Schneidkörper auf der Vorderseite" width="1200" height="800" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Nach der Differenz</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="Körper mit ausgeschnittener Mulde und dunklem Display darin" width="1200" height="800" loading="lazy" decoding="async" /></picture></div></div><figcaption>Der blaue Körper wird abgezogen. Rechts liegt das Display bereits in der Mulde.</figcaption></figure>
<p>Zwei praktische Details:</p>
<ul>
<li><strong>Der Quader für den Schnitt muss wirklich groß sein.</strong> Er muss nur die Tiefe beschneiden, in Breite und Höhe aber das ganze Gerät überragen. Leiten Sie ihn nicht von der Tiefe ab: Das 20-Fache der Tiefe ist bei einem dünnen, hohen Handy zum Beispiel weniger als seine Höhe, und oben und unten bleibt ein unbeschnittener Rahmen stehen.</li>
<li><strong>Vergrößern Sie den Schneidkörper um ein paar Promille.</strong> Wenn zwei Körper exakt dieselbe Fläche haben, kann CSG nicht entscheiden, welche Seite innen liegt. Ein Maßstab von 1,002 verhindert das und ist mit bloßem Auge nicht zu sehen.</li>
</ul>
<h2 id="schritt-3-seitliche-elemente-als-bohrer-vorbereiten"><a class="heading-anchor" href="#schritt-3-seitliche-elemente-als-bohrer-vorbereiten" aria-hidden="true" tabindex="-1">#</a>Schritt 3: Seitliche Elemente als Bohrer vorbereiten</h2>
<p>Jedes Element an der Seite folgt demselben Rezept mit einem anderen Bohrer. Eine Taste entsteht zum Beispiel aus drei Körpern: Ein abgerundetes Rechteck, etwas tiefer als die Taste, wird vom Rahmen abgezogen, der Rand der Öffnung erhält eine kleine Fase, und in die Öffnung kommt ein etwas kleinerer Körper, der leicht aus dem Rahmen ragt.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Bohrer</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="Seite eines Handys mit zwei blauen Schneidkörpern für Tasten" width="1200" height="800" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Ausschnitt und Taste</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="Seite eines Handys mit ausgeschnittenen Öffnungen und eingesetzten Tasten" width="1200" height="800" loading="lazy" decoding="async" /></picture></div></div><figcaption>Zwei Tasten auf der rechten Seite vor und nach der Differenz.</figcaption></figure>
<p>Lautsprecher sind eine Reihe kleiner Zylinder, der Anschluss ist ein abgerundetes Rechteck. Setzen Sie in jede Öffnung etwas Dunkles: eine dünne Scheibe ins Lautsprechergitter, eine Platte in den Anschluss. Ein leeres Loch in einer dünnen Schale wirkt wie ein Fehler, ein Loch mit dunkler Füllung wirkt wie 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="Unterkante eines Handys mit zwei Reihen Lautsprecherlöchern und einem Anschluss in der Mitte" width="1200" height="800" loading="lazy" decoding="async" /></picture><figcaption>Lautsprecherlöcher und Anschluss. Alles abgezogene Zylinder und abgerundete Rechtecke.</figcaption></figure>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">für jedes Element in der Liste der Seitenelemente:
    Bohrer = Form des Elements (Zylinder, abgerundetes Rechteck, ...)
    setze den Bohrer an seiner Position in mm auf die Kante
    füge den Bohrer zur Liste der Schnitte hinzu
    wenn das Element eine Füllung hat: füge die Füllung zur Szene hinzu</code></pre>
<p>Eine Taste hinzuzufügen ist dann ein Dateneintrag. Die Elemente summieren sich schnell: Ein iPad Pro 13″ hat 58 Lautsprecherlöcher, 4 Tasten, 2 Antennen und einen Anschluss, also über 60 Differenzen an einem einzigen Körper.</p>
<h2 id="schritt-4-details-auf-der-vorderseite-nicht-bohren"><a class="heading-anchor" href="#schritt-4-details-auf-der-vorderseite-nicht-bohren" aria-hidden="true" tabindex="-1">#</a>Schritt 4: Details auf der Vorderseite nicht bohren</h2>
<p>Die Kameraaussparung, die Insel oben im Display und der Home-Indikator sind flach. Dafür Geometrie zu schneiden ist verschwendete Arbeit. Bringen Sie sie stattdessen als Decal an, also als Bild, das auf eine Oberfläche projiziert wird. Projizieren Sie es auf eine unsichtbare Ebene über die ganze Vorderseite und geben Sie die Position in denselben Millimetern an wie alles andere. Die Ausnahme ist der runde Home-Button älterer iPhones. Er hat echte Tiefe, die man von der Seite sieht, und geht deshalb durch 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">Ausprobieren</p><h2 class="app-box-title">Ecranify</h2><p>Ein Browser-Tool für fotorealistische 3D-Gerätemockups für App-Store- und Google-Play-Screenshots. WebGL-Notizen und Neuigkeiten.</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="schritt-5-die-berechnung-in-einen-web-worker-verlagern"><a class="heading-anchor" href="#schritt-5-die-berechnung-in-einen-web-worker-verlagern" aria-hidden="true" tabindex="-1">#</a>Schritt 5: Die Berechnung in einen Web Worker verlagern</h2>
<p>JavaScript läuft im Browser in einem einzigen Thread. Derselbe Thread rechnet, zeichnet die Seite und reagiert auf Klicks. Solange CSG läuft, steht die Seite still, und auch ein Spinner hilft nicht, weil er denselben Thread braucht.</p>
<p>Starten Sie deshalb den gesamten Aufbau des Geräts in einem <a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API" target="_blank" rel="noopener">Web Worker</a>, also in einem eigenen Hintergrund-Thread. Der Worker erhält die Gerätebeschreibung, gibt fertige Vertex-Arrays zurück, und die Seite bleibt flüssig. Wenn sich ein Parameter ändert, beenden Sie den alten Worker und starten einen neuen, damit er keinen veralteten Zustand zu Ende rechnet.</p>
<p>Das Ergebnis aus dem Worker dauert einen Moment, deshalb ergänzen Sie eine sofortige Vorschau. Zeigen Sie das bearbeitete Element als blauen, durchscheinenden „Geist“, einen einfachen Quader oder Zylinder mit ein paar Dutzend Dreiecken, der sich im Haupt-Thread sofort aufbauen lässt. Wenn der Worker fertig ist, verschwindet der Geist.</p>
<h2 id="schritt-6-alle-locher-auf-einmal-abziehen"><a class="heading-anchor" href="#schritt-6-alle-locher-auf-einmal-abziehen" aria-hidden="true" tabindex="-1">#</a>Schritt 6: Alle Löcher auf einmal abziehen</h2>
<p>Am direktesten ist es, die Elemente nacheinander abzuziehen: Loch 1 vom Körper, dann Loch 2 vom Ergebnis und so weiter. Jede Differenz baut aber den räumlichen Index des ganzen Körpers neu auf, und das ist der teuerste Teil der Berechnung. Bei Dutzenden Elementen wächst die Zeit sehr schnell.</p>
<p>Besser ist es, zuerst alle Bohrer für einen Körper zu sammeln, sie zu einem Mesh zu verbinden und einmal abzuziehen.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">Schnitte = []
für jedes Element: füge seinen Bohrer zu Schnitte hinzu
verbunden = verbinde alle Schnitte zu einem Mesh
Körper = Körper minus verbunden
wenn das Verbinden scheitert: ziehe die Schnitte einzeln ab</code></pre>
<p>Gemessen an einer vereinfachten Kopie des Ablaufs (Three.js 0.170, three-bvh-csg 0.0.17, Node 22, Cloud-Server mit 2 Kernen Intel Xeon 2,1 GHz, Median aus 5 Läufen):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Körper</th>
<th>Löcher</th>
<th>Einzeln</th>
<th>Auf einmal</th>
</tr>
</thead>
<tbody><tr>
<td>Handy, 17.676 Dreiecke</td>
<td>25</td>
<td>2,47 s</td>
<td>0,26 s</td>
</tr>
<tr>
<td>Tablet, 17.676 Dreiecke</td>
<td>58</td>
<td>5,55 s</td>
<td>0,89 s</td>
</tr>
<tr>
<td>Tablet, 68.108 Dreiecke</td>
<td>58</td>
<td>22,8 s</td>
<td>3,1 s</td>
</tr>
</tbody></table>
</div>
<p>Eine Bedingung: Verbinden und einmal Abziehen liefert nur dann dasselbe Ergebnis wie das schrittweise Abziehen, wenn sich die Bohrer nicht überlappen. Die Elemente sitzen an verschiedenen Stellen, also gilt das in der Regel. Schiebt ein Nutzer aber zwei Elemente übereinander, kann an der Überlappung ein kleiner Fehler entstehen.</p>
<h2 id="schritt-7-die-segmentzahl-als-parameter-fuhren"><a class="heading-anchor" href="#schritt-7-die-segmentzahl-als-parameter-fuhren" aria-hidden="true" tabindex="-1">#</a>Schritt 7: Die Segmentzahl als Parameter führen</h2>
<p>Eine Tessellation, die für die Anzeige passt, ist für CSG oft unnötig dicht. Mit 64 Segmenten pro Ecke und 32 für den Bevel hat der Körper 68.108 Dreiecke. Die Hälfte (32 und 16) sieht man an einer 2-mm-Kante nicht, der Körper hat dann aber nur 17.676 Dreiecke, und die dritte Zeile der Tabelle zeigt, was das für die Zeit bedeutet.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">64 und 32 Segmente</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="Drahtgittermodell einer Handyecke mit sehr dichtem Dreiecksnetz" width="1200" height="800" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">32 und 16 Segmente</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="Drahtgittermodell derselben Ecke mit halb so vielen Segmenten" width="1200" height="800" loading="lazy" decoding="async" /></picture></div></div><figcaption>Die Ecke des Körpers von oben. Rechts ist es ein Viertel der Dreiecke.</figcaption></figure>
<p>Geben Sie kleinen Elementen mit einem Radius bis 2 mm (Lautsprecherlöcher, Schrauben) die Hälfte der Segmente, Vorschaubildern in der Galerie noch weniger. Legen Sie die Qualität deshalb nicht als Konstante im Code fest, sondern als Parameter des Aufbaus.</p>
<h2 id="rendern-auf-einem-server-ohne-grafikkarte"><a class="heading-anchor" href="#rendern-auf-einem-server-ohne-grafikkarte" aria-hidden="true" tabindex="-1">#</a>Rendern auf einem Server ohne Grafikkarte</h2>
<p>Wenn Sie dieselbe Szene auch auf einem Server rendern müssen, etwa als Backup für Exporte, läuft sie in Chrome ohne Bildschirm (headless), das ohne Grafikkarte in Software zeichnet. Dort vervielfacht sich jede Ineffizienz. In Ecranify lief das schwerste Tablet ohne die genannten Anpassungen in das Limit von 15 Minuten. Mit verbundenen Schnitten, kleineren Texturen, geringerer Tessellation und einem Cache fertiger Geometrie, dessen Schlüssel ein Hash der Gerätebeschreibung ist, dauert ein Render etwa eine halbe Minute und ein wiederholter etwa 5 Sekunden. Die Details mit interaktiven Beispielen stehen im <a href="https://ecranify.com/guides/how-we-build-3d-device-mockups" target="_blank" rel="noopener">englischen Artikel auf ecranify.com</a>.</p>
<p>Wenn Sie ein ähnliches 3D-Problem im Browser lösen oder eine App nach Maß brauchen, <a href="https://smallwater.eu/de/#kontakt">schreiben Sie uns</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Wie Sie ein Unity-4-Projekt auf Unity 6 umstellen</title>
      <link>https://smallwater.eu/de/blog/unity-4-to-unity-6-upgrade/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/unity-4-to-unity-6-upgrade/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Onager</category>
      <description>So bringen Sie ein Unity-4-Projekt ohne verlorene Szenen auf Unity 6: Textkonvertierung in Unity 2019.4, C#-Port mit gleicher GUID und ein gerettetes 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="Ein Bauwerk aus Blöcken in einem Onager-Level auf einem Boden aus Milchglas" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#was-passiert-wenn-sie-das-projekt-in-unity-6-offnen" data-ga="blog_toc_clicked" data-ga-heading-id="was-passiert-wenn-sie-das-projekt-in-unity-6-offnen">Was passiert, wenn Sie das Projekt in Unity 6 öffnen</a></li><li><a href="#schritt-1-den-ausgangszustand-festhalten" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-1-den-ausgangszustand-festhalten">Schritt 1: Den Ausgangszustand festhalten</a></li><li><a href="#schritt-2-uber-unity-2019-4-in-text-konvertieren" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-2-uber-unity-2019-4-in-text-konvertieren">Schritt 2: Über Unity 2019.4 in Text konvertieren</a></li><li><a href="#schritt-3-das-ergebnis-im-editor-prufen-nicht-im-yaml" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-3-das-ergebnis-im-editor-prufen-nicht-im-yaml">Schritt 3: Das Ergebnis im Editor prüfen, nicht im YAML</a></li><li><a href="#schritt-4-unityscript-nach-c-mit-derselben-guid" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-4-unityscript-nach-c-mit-derselben-guid">Schritt 4: UnityScript nach C# mit derselben GUID</a></li><li><a href="#schritt-5-das-hud-aus-den-ursprunglichen-binarszenen-wiederherstellen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-5-das-hud-aus-den-ursprunglichen-binarszenen-wiederherstellen">Schritt 5: Das HUD aus den ursprünglichen Binärszenen wiederherstellen</a></li><li><a href="#nach-der-konvertierung-alte-behelfslosungen-ersetzen" data-ga="blog_toc_clicked" data-ga-heading-id="nach-der-konvertierung-alte-behelfslosungen-ersetzen">Nach der Konvertierung: alte Behelfslösungen ersetzen</a></li><li><a href="#was-dieses-vorgehen-nicht-lost" data-ga="blog_toc_clicked" data-ga-heading-id="was-dieses-vorgehen-nicht-lost">Was dieses Vorgehen nicht löst</a></li></ol></nav><p>Öffnen Sie ein Unity-4-Projekt nicht direkt in Unity 6 und speichern Sie darin vor allem nichts. Unity 6 lädt die alten binären Szenen nur teilweise, und das Speichern macht den Verlust endgültig; der sichere Weg führt über Unity 2019.4, das das alte Format lesen und als Text zurückschreiben kann, und erst danach zu Unity 6.</p>
<p>Das betrifft Projekte, deren Szenen und Prefabs im Binärformat gespeichert sind und deren Skripte in UnityScript geschrieben wurden, einer JavaScript-ähnlichen Sprache, die Unity früher neben C# unterstützte. Als Beispiel dient Onager, ein Physikspiel mit Katapult, geschrieben in Unity 4.1.2 und UnityScript. Das Projekt hatte 126 Skripte, 75 Szenen und 78 Prefabs. Nach der Umstellung läuft es auf Unity 6000.5 mit URP (Universal Render Pipeline) und ist kostenlos im <a href="https://smallwater.eu/onager-ios">App Store</a> und bei <a href="https://smallwater.eu/onager-android">Google Play</a>.</p>
<h2 id="was-passiert-wenn-sie-das-projekt-in-unity-6-offnen"><a class="heading-anchor" href="#was-passiert-wenn-sie-das-projekt-in-unity-6-offnen" aria-hidden="true" tabindex="-1">#</a>Was passiert, wenn Sie das Projekt in Unity 6 öffnen</h2>
<p>Der einfachste Ansatz ist, das Projekt in Unity 6 zu öffnen und die Serialisierung auf Force Text umzustellen, also Assets als lesbares YAML zu speichern. Das ist aber keine Übersetzung der Dateien. Unity lädt jedes Asset über seinen Importer und schreibt es als YAML zurück, alles läuft also durch den Loader von Unity 6. Der meldet in jeder Szene einen Transform ohne GameObject und löscht die „hängende“ Komponente. Beim Speichern wird das endgültig.</p>
<p>Den Verlust können Sie mit binary2text messen, einem Werkzeug, das dem Editor beiliegt und eine serialisierte Datei ohne Importer liest. Bei Onager haben wir die Ausgabe mit dem entstandenen YAML verglichen und nur Komponenten gezählt, die von überlebenden GameObjects verschwunden sind:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Komponente</th>
<th align="right">Verloren</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>Keine der 75 Szenen kam ohne Verlust durch. Ganze HUD-Kameras verschwanden, und an die Stelle der Hauptkamera trat eine neue Standardkamera. Wichtig ist: Der Schaden entsteht schon beim Laden, nicht beim Speichern. Solange das Projekt nicht konvertiert ist, darf Unity 6 es nicht speichern, keine einzige Szene und kein einziges Prefab.</p>
<h2 id="schritt-1-den-ausgangszustand-festhalten"><a class="heading-anchor" href="#schritt-1-den-ausgangszustand-festhalten" aria-hidden="true" tabindex="-1">#</a>Schritt 1: Den Ausgangszustand festhalten</h2>
<p>Bevor Sie irgendetwas anfassen, machen Sie von jeder Szene einen Screenshot und speichern Sie für jede ursprüngliche Szenendatei einen Dump mit binary2text. Mit diesen Dumps prüfen Sie in Schritt 3 die Kameras und stellen in Schritt 5 das HUD wieder her. Lassen Sie die ursprünglichen Binärdateien in der git-Historie und löschen Sie sie nicht.</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="Das erste Level in Unity 6 vor dem Remaster" width="1280" height="720" loading="lazy" decoding="async" /></picture><figcaption>Das erste Level von Onager in Unity 6, noch mit altem Renderer und den Originalmaterialien. Halten Sie jede Szene so fest, bevor Sie etwas anfassen.</figcaption></figure>
<h2 id="schritt-2-uber-unity-2019-4-in-text-konvertieren"><a class="heading-anchor" href="#schritt-2-uber-unity-2019-4-in-text-konvertieren" aria-hidden="true" tabindex="-1">#</a>Schritt 2: Über Unity 2019.4 in Text konvertieren</h2>
<p>Unity 2019.4 ist der letzte Editor, der das Format von Unity 4.1 lesen und schreiben kann. Konvertieren Sie bei geschlossenem Editor in einer separaten Kopie des Repositorys:</p>
<ul>
<li><strong>Projektversion.</strong> Tragen Sie in der Datei mit der Projektversion die ursprüngliche Version ein (bei Onager 4.1.2), damit der Editor den Weg für alte Projekte nimmt. Unity überschreibt sie beim Öffnen, sie muss nur für den ersten Import gelten.</li>
<li><strong>Leere Pakete.</strong> Leeren Sie die Liste der Paketabhängigkeiten. Ein einziger Kompilierfehler bedeutet, dass kein Menüeintrag aus Editor-Skripten funktioniert, und das sieht aus, als wäre das Menü nie erschienen.</li>
<li><strong>Import von Grund auf.</strong> Löschen Sie den Ordner Library und lassen Sie das Projekt importieren.</li>
<li><strong>Force Text und erzwungenes Neuschreiben.</strong> Unity 2019.4 schreibt vorhandene Dateien nach dem Umstellen auf Force Text nicht von selbst neu. Sie müssen das mit der Funktion zur Reserialisierung von Assets auslösen, und zwar nur für die Assets, nicht für ihre Meta-Dateien. Das Neuschreiben aller Meta-Dateien führt dazu, dass Unity 6 danach endlos neu importiert.</li>
</ul>
<p>Eine Falle steckt in git. Wenn Ihre git-Attribute Szenen als Text mit LF-Zeilenenden markieren, entfernt git beim Hinzufügen zum Repository aus binären Szenen Bytes, die wie Zeilenenden aussehen. Bei Onager hat das acht Szenen beschädigt. Setzen Sie für Unity-Assets kein Zeilenende-Attribut, Unity schreibt ohnehin LF.</p>
<h2 id="schritt-3-das-ergebnis-im-editor-prufen-nicht-im-yaml"><a class="heading-anchor" href="#schritt-3-das-ergebnis-im-editor-prufen-nicht-im-yaml" aria-hidden="true" tabindex="-1">#</a>Schritt 3: Das Ergebnis im Editor prüfen, nicht im YAML</h2>
<p>Das Lesen der konvertierten Dateien führt zu selbstsicheren falschen Schlüssen. Verkürzte Einträge von Prefab-Instanzen sehen wie ein Verlust aus, ebenso eine Kamera, die nur in ein Prefab gewandert ist. Entscheidend ist, was Unity im Speicher aufbaut. Öffnen Sie jede konvertierte Szene und vergleichen Sie ihre Kameras mit dem Dump des Originals aus Schritt 1.</p>
<p>Bei Onager hatten danach alle 75 Szenen dieselben Kameras in derselben Reihenfolge. Verloren ging nur, was kein heutiger Editor mehr kennt: 569 GUITexture, 179 GUIText und die alten Partikelsysteme, die Unity 2018 entfernt hat.</p>
<h2 id="schritt-4-unityscript-nach-c-mit-derselben-guid"><a class="heading-anchor" href="#schritt-4-unityscript-nach-c-mit-derselben-guid" aria-hidden="true" tabindex="-1">#</a>Schritt 4: UnityScript nach C# mit derselben GUID</h2>
<p>Unity hat <a href="https://blog.unity.com/community/unityscripts-long-ride-off-into-the-sunset" target="_blank" rel="noopener">UnityScript ab Version 2017.2 schrittweise abgeschafft</a>, ein manueller Port nach C# ist also der einzige Weg. Szenen verweisen auf Skripte über die GUID, eine eindeutige Kennung, die Unity in der Meta-Datei jedes Assets speichert, und speichern Feldwerte nach dem Feldnamen. Wenn Sie beides beim Port beibehalten, verbinden sich alle Verweise von selbst:</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">für jedes UnityScript-Skript X:
  lege ein C#-Skript mit einer Klasse namens genau X an
  kopiere die GUID aus der Meta-Datei des alten Skripts in die des neuen
  öffentliche Felder: gleicher Name, gleicher Typ, auch mit Tippfehlern
  nichts umbenennen, bis das Projekt läuft</code></pre>
<p>Bei Onager verbanden sich so nach dem Port aller 126 Skripte 12.935 Komponenten in 73 Szenen, und alle im Inspector gesetzten Werte blieben erhalten.</p>
<p>Per Textsuche im Projekt lässt sich nicht ermitteln, welche Skripte verwendet werden. Ein binäres Prefab speichert die GUID als Bytes, nicht als Text. Bei Onager wirkten so einundzwanzig Skripte ungenutzt, und alle waren aktiv. Achten Sie auch auf Reste früherer Port-Versuche: Ein Holzblock-Prefab zeigte auf zwei Skripte aus einem solchen Versuch, und allein dadurch fehlten 1.140 Komponenten. Wenn Sie in einer Binärdatei eine GUID ändern müssen, tauschen Sie sie direkt in den Bytes, sechzehn gegen sechzehn, damit sich die Datei nicht verschiebt.</p>
<h2 id="schritt-5-das-hud-aus-den-ursprunglichen-binarszenen-wiederherstellen"><a class="heading-anchor" href="#schritt-5-das-hud-aus-den-ursprunglichen-binarszenen-wiederherstellen" aria-hidden="true" tabindex="-1">#</a>Schritt 5: Das HUD aus den ursprünglichen Binärszenen wiederherstellen</h2>
<p>GUITexture und GUIText gibt es in Unity 6 nicht, und mit ihnen verschwindet das ganze darauf aufgebaute HUD. Neu zeichnen müssen Sie es nicht. binary2text liest die ursprünglichen Binärszenen aus der git-Historie unabhängig davon, ob die Klassen noch existieren. Ziehen Sie daraus jedes HUD-Bild und jeden HUD-Text mit Textur, Farbe, Position und Ausrichtung und setzen Sie sie per Skript auf dieselben Objekte in den Szenen zurück. Bei Onager waren es 577 Bilder und 180 Texte.</p>
<p>So retten Sie auch Texte, die es nirgendwo sonst gibt. Bei Onager waren es 111 Texte: alle Tutorial-Untertitel und die ganze Credits-Tafel. Ein HUD von Grund auf zu schreiben hätte bedeutet, sie zu erfinden. Beim Übertragen der Farben gilt etwas, das die Dokumentation nicht sagt: Das alte GUITexture verdoppelte die Farbe beim Zeichnen. Alle 577 Bilder hatten eine Farbe von genau 0,5, Grau bedeutete also Weiß.</p>
<h2 id="nach-der-konvertierung-alte-behelfslosungen-ersetzen"><a class="heading-anchor" href="#nach-der-konvertierung-alte-behelfslosungen-ersetzen" aria-hidden="true" tabindex="-1">#</a>Nach der Konvertierung: alte Behelfslösungen ersetzen</h2>
<p>Alte Projekte umgingen oft die Grenzen des damaligen Unity, und diese Behelfslösungen lohnt es sich nach der Konvertierung neu zu bauen:</p>
<ul>
<li><strong>Spiegelung im Boden.</strong> Onager hatte einen glänzenden Boden ohne echte Spiegelung. Unter dem Boden stand eine zweite, gespiegelte Welt: eine Kopie jedes Objekts, deren Position ein Skript in jedem dritten Frame vom Original übernahm, in einem Level 50 bis 70 Klone. Ersetzen lässt sich das durch eine zusätzliche Kamera, die die Spiegelung in halber Auflösung zeichnet. Dann spiegeln sich auch Partikel und Trümmer, was die geklonte Welt nicht konnte.</li>
<li><strong>Zielen.</strong> Wenn die Flugbahnvorschau vorhersagen soll, wohin eine im ersten Physikschritt hinzugefügte Kraft den Schuss schickt, landet er woanders als die Markierung. Zuverlässiger ist es, die Geschwindigkeit einmal zu berechnen, die Vorschau genau daraus zu zeichnen und sie dem Schuss direkt zuzuweisen.</li>
<li><strong>Leistung.</strong> Messen Sie auf einem schwachen Handy. Auf einem Motorola G52 fiel Onager allein wegen des Wand-Shaders von 20 auf 6 fps, mehr als Auflösung, Schatten und Spiegelung zusammen. Bei niedriger und mittlerer Detailstufe haben die Wände deshalb ein einfacheres Material.</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="Der Gravitationsschuss im konvertierten Onager: Eine Kugel hebt die Blöcke an, umgeben von einem Wirbel und blauem Licht."><source src="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/gravity-shot.mp4" type="video/mp4" /></video><figcaption>Der Gravitationsschuss im konvertierten Onager: Eine Kugel hebt die Blöcke an, umgeben von einem Wirbel und blauem Licht.</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">Ausprobieren</p><h2 class="app-box-title">Onager</h2><p>Ein 3D-Physik-Puzzlespiel mit Katapult: 60 Level voller Zerstörung. Physik, Leveldesign und Release Notes.</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="Der Regenbogenschuss: Weiße Punkte treten direkt aus der Oberfläche des getroffenen Blocks aus."><source src="https://smallwater.eu/blog/unity-4-to-unity-6-upgrade/rainbow-shot.mp4" type="video/mp4" /></video><figcaption>Der Regenbogenschuss: Weiße Punkte treten direkt aus der Oberfläche des getroffenen Blocks aus.</figcaption></figure>
<p>Wenn Sie im aktuellen Unity Physik für Tausende Objekte lösen, lesen Sie auch über den <a href="https://smallwater.eu/de/blog/fake-ragdoll-unity-dots/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/unity-4-to-unity-6-upgrade/" data-ga-destination="/de/blog/fake-ragdoll-unity-dots/" data-ga-placement="inline">Fake-Ragdoll für Tausende Gegner in Unity DOTS</a>. Wie Sie einem alten 2D-Spiel nach der Umstellung in URP Glow und 2D-Lichter geben, beschreibt eine <a href="https://smallwater.eu/de/blog/dreamica-remaster-unity-urp-glow/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/unity-4-to-unity-6-upgrade/" data-ga-destination="/de/blog/dreamica-remaster-unity-urp-glow/" data-ga-placement="inline">eigene Anleitung</a>.</p>
<h2 id="was-dieses-vorgehen-nicht-lost"><a class="heading-anchor" href="#was-dieses-vorgehen-nicht-lost" aria-hidden="true" tabindex="-1">#</a>Was dieses Vorgehen nicht löst</h2>
<p>Komponenten, die das heutige Unity nicht kennt, verbinden sich nicht. GUITexture und GUIText müssen Sie wie in Schritt 5 wiederherstellen, die alten Partikelsysteme gehen verloren. Klassen- und Feldnamen bleiben genau wie im ursprünglichen Projekt, mit Tippfehlern und in der Sprache, in der sie jemand einmal geschrieben hat. Szenen binden über den Namen an sie, daher ist Umbenennen nur über den Editor sinnvoll, der die Verweise selbst umschreibt.</p>
<p>Wenn Sie ein altes Unity-Projekt auf eine aktuelle Version bringen müssen, <a href="https://smallwater.eu/de/#kontakt">schreiben Sie uns</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Wie man Tausende Gegner in Unity DOTS navigiert</title>
      <link>https://smallwater.eu/de/blog/unity-dots-horde-navigation/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/unity-dots-horde-navigation/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Tower Defense</category>
      <description>Anleitung für Tausende Gegner in Unity DOTS: Flow Field, Abstoßen von Nachbarn und Wänden, Kosten nach Dichte, Staus an Engstellen und Performance.</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="Eine Horde von Gegnern als helle Punkte umfließt Hindernisse zum Ziel, darunter Pfeile des Flow Fields und eine rote Dichtekarte" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#schritt-1-flow-field-statt-eines-pfads-pro-einheit" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-1-flow-field-statt-eines-pfads-pro-einheit">Schritt 1: Flow Field statt eines Pfads pro Einheit</a></li><li><a href="#schritt-2-ein-eigenes-feld-fur-jede-gegnergro-e" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-2-ein-eigenes-feld-fur-jede-gegnergro-e">Schritt 2: Ein eigenes Feld für jede Gegnergröße</a></li><li><a href="#schritt-3-absto-en-von-nachbarn" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-3-absto-en-von-nachbarn">Schritt 3: Abstoßen von Nachbarn</a></li><li><a href="#schritt-4-absto-en-von-wanden" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-4-absto-en-von-wanden">Schritt 4: Abstoßen von Wänden</a></li><li><a href="#schritt-5-kosten-nach-dichte-und-abbremsen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-5-kosten-nach-dichte-und-abbremsen">Schritt 5: Kosten nach Dichte und Abbremsen</a></li><li><a href="#schritt-6-stau-in-der-engstelle-warten-statt-drucken" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-6-stau-in-der-engstelle-warten-statt-drucken">Schritt 6: Stau in der Engstelle: warten statt drücken</a></li><li><a href="#schritt-7-gro-e-gegner-uberholen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-7-gro-e-gegner-uberholen">Schritt 7: Große Gegner überholen</a></li><li><a href="#schritt-8-slots-am-angriffsziel" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-8-slots-am-angriffsziel">Schritt 8: Slots am Angriffsziel</a></li><li><a href="#performance-von-simulation-und-rendering" data-ga="blog_toc_clicked" data-ga-heading-id="performance-von-simulation-und-rendering">Performance von Simulation und Rendering</a></li><li><a href="#grenzen-des-ansatzes" data-ga="blog_toc_clicked" data-ga-heading-id="grenzen-des-ansatzes">Grenzen des Ansatzes</a></li></ol></nav><p>Steuern Sie eine Horde aus Tausenden Gegnern in Unity DOTS mit einem einzigen Flow Field für die ganze Karte und ein paar günstigen lokalen Regeln: Abstoßen von Nachbarn und Wänden, Wegkosten nach Dichte der Menge und ein Zustand „warten“ im Stau. Im Unity Editor auf einem MacBook mit M3 Pro schafft das rund 9.600 Gegner gleichzeitig bei 90 FPS.</p>
<p>Das Problem tritt in Tower-Defense-, Strategie- und Survival-Spielen auf, in denen Tausende Einheiten gleichzeitig auf ein Ziel zulaufen. Klassisches Pathfinding für jede Einheit einzeln ist zu teuer, und ohne lokale Regeln überlappt sich die Menge, klebt an Wänden und bleibt in Engstellen hängen. Unity DOTS (Data-Oriented Technology Stack, also Entities, ECS und das Job System) liefert die Leistung für Tausende Entities, die Navigation selbst löst es aber nicht. Wir nutzen das folgende Vorgehen in unserem Tower-Defense-Spiel für PC (es steht in unseren <a href="https://smallwater.eu/de/#referencie">Referenzen</a>). Die Bilder stammen aus einer vereinfachten 2D-Simulation mit denselben Regeln, nicht aus dem Spiel.</p>
<h2 id="schritt-1-flow-field-statt-eines-pfads-pro-einheit"><a class="heading-anchor" href="#schritt-1-flow-field-statt-eines-pfads-pro-einheit" aria-hidden="true" tabindex="-1">#</a>Schritt 1: Flow Field statt eines Pfads pro Einheit</h2>
<p>Ein Flow Field ist ein Raster über die ganze Karte, in dem jede Zelle weiß, in welche Richtung es von ihr zum Ziel geht. Ein Gegner sucht keinen Pfad, er liest nur die Richtung an der Stelle ab, an der er steht. Die Kosten sind für 10 und für 10.000 Gegner gleich.</p>
<p>Teilen Sie die Karte in Zellen von 1 m auf (unser Testlevel hat 128 × 128) und berechnen Sie für jedes Ziel einmal die Entfernung jeder Zelle zum Ziel. Ein statisches Feld lässt sich im Editor bei der Vorbereitung des Levels berechnen und in einem Asset speichern.</p>
<p>Die einfachste Variante ist Dijkstra über 8 Nachbarn und in jeder Zelle eine von 8 Richtungen. Das funktioniert, aber die Gegner laufen diagonal und kleben im Gänsemarsch an Wänden.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">8 Richtungen</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 mit acht Richtungen, die Wege der Gegner verlaufen diagonal und kleben an der Kante des Hindernisses" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Glattes Feld</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="Glattes Flow Field, die Wege laufen im Bogen um die Ecke des Hindernisses mit Abstand zur Wand zusammen" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div></div><figcaption>Gleiche Karte, Wege einzelner Gegner. Links 8 Richtungen, rechts ein glattes Feld mit Aufschlag an Wänden.</figcaption></figure>
<p>Ein glattes Feld liefert ein besseres Ergebnis. Berechnen Sie die Entfernung durch Lösen der <a href="https://en.wikipedia.org/wiki/Eikonal_equation" target="_blank" rel="noopener">Eikonal-Gleichung</a> (eine Wellenfront-Gleichung, deren Lösung die kürzeste Entfernung im kontinuierlichen Raum ist, nicht nur entlang der Rasterkanten) mit der Fast-Sweeping-Methode, die das Raster wiederholt von allen vier Ecken aus durchläuft. Die Richtung ist dann nicht die Richtung der Zelle, sondern der Gradient der Entfernung an der genauen Position des Gegners. Schlagen Sie auf die Kosten einer Zelle einen Aufschlag für Wandnähe auf, dann hält das Feld die Menge von selbst etwas von Hindernissen fern.</p>
<h2 id="schritt-2-ein-eigenes-feld-fur-jede-gegnergro-e"><a class="heading-anchor" href="#schritt-2-ein-eigenes-feld-fur-jede-gegnergro-e" aria-hidden="true" tabindex="-1">#</a>Schritt 2: Ein eigenes Feld für jede Gegnergröße</h2>
<p>Ein großer Gegner passt nicht überall hin, wo ein kleiner hinpasst. Führen Sie Größenklassen ein: Jede hat eine eigene Begehbarkeit (eine Zelle muss weit genug von der Wand entfernt sein) und ein eigenes Feld pro Ziel. Halten Sie die Klassen als Leveldaten, nicht im Code. Bei uns sind es drei.</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="Karte mit schmalem Gang: kleine Gegner gehen hindurch, größere orangefarbene umgehen ihn unten" width="1792" height="1008" loading="lazy" decoding="async" /></picture><figcaption>Kleine Gegner nehmen den schmalen Gang, große (orange) passen nicht hinein und gehen außen herum.</figcaption></figure>
<h2 id="schritt-3-absto-en-von-nachbarn"><a class="heading-anchor" href="#schritt-3-absto-en-von-nachbarn" aria-hidden="true" tabindex="-1">#</a>Schritt 3: Abstoßen von Nachbarn</h2>
<p>Ein Flow Field gibt die Richtung vor, weiß aber nichts über Nachbarn. Die Grundlage ist ein vereinfachtes Boids-Modell (die klassischen Schwarmregeln): Jeder Gegner stößt sich von Nachbarn im Umkreis von 0,9 m ab. Suchen Sie Nachbarn in einem Spatial Hash, einem Raster, in dem jede Zelle eine Liste der Einheiten darin hält. Bauen Sie den Hash jeden Tick neu auf und sortieren Sie ihn, damit die Reihenfolge deterministisch bleibt.</p>
<p>Abstoßung allein reicht nicht. Bei 3.000 Gegnern hatten 83,7 % ihren nächsten Nachbarn näher als 0,8 m, und der kleinste Abstand lag bei 4 cm, also zwei Gegner fast ineinander.</p>
<p>Ein höheres Gewicht der Abstoßung löst das nicht. Bei Gewicht 6 statt 1,5 sank der Anteil der zu dicht stehenden auf 54,6 %, bei 12 auf 44,3 %, bei 24 brachte es nichts mehr (48,2 %). Dazu beginnt die Menge zu zappeln, weil sich unbegrenzte Stöße vieler Nachbarn gleichzeitig addieren.</p>
<p>Eine Positionskorrektur hilft. Schieben Sie nach der Bewegung jedes überlappende Paar auseinander, jeden um die Hälfte der Überlappung, und begrenzen Sie die Verschiebung auf 0,15 m pro Tick. Ohne diese Grenze springt die Korrektur genauso wie das hohe Gewicht. Mit ihr bleiben 60,8 % zu dicht, aber der kleinste Abstand steigt auf 28 cm und die Menge bleibt ruhig.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Nur Abstoßung</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="Menge an einer Engstelle, ein großer Teil der Gegner ist rot, sie überlappen sich" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Überlappungskorrektur</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="Dieselbe Menge mit Überlappungskorrektur, rot sind nur noch die direkt vor der Engstelle" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div></div><figcaption>Rote Gegner überlappen einen Nachbarn um mehr als 20 cm. Auf freier Fläche hilft die Korrektur, vor der Engstelle nicht.</figcaption></figure>
<h2 id="schritt-4-absto-en-von-wanden"><a class="heading-anchor" href="#schritt-4-absto-en-von-wanden" aria-hidden="true" tabindex="-1">#</a>Schritt 4: Abstoßen von Wänden</h2>
<p>An der Wand versagt die Korrektur zwischen Gegnern. Bewegung in die Wand ist verboten (die Verschiebung wird nach Achsen zerlegt und der Anteil in die Wand verworfen), also kann ein angedrückter Gegner nirgends ausweichen und die Korrektur vom Nachbarn geht verloren. Von 938 überlappenden Gegnern hatten nur 35 überhaupt einen Druck von der Wand.</p>
<p>Fügen Sie deshalb ein Abstoßen von den nächsten nicht begehbaren Zellen hinzu, in 4 Richtungen und diagonal. Zwei Fehler lassen sich vermeiden:</p>
<ul>
<li><strong>Innenecke.</strong> Der Druck von beiden Kanten und von der Ecke addiert sich und schleudert den Gegner weg. Fügen Sie den Eckdruck nur an einer Außenecke hinzu.</li>
<li><strong>Echte Wände.</strong> Stoßen Sie von den echten Wänden ab, nicht von der Begehbarkeit der Größenklasse. Für einen großen Gegner beginnt die „Wand“ schon einen Meter vor der echten. Am Eingang eines Gangs hob der Eckdruck so die Vorwärtsbewegung eines großen Gegners auf, und die ganze Gruppe hinter ihm blieb stehen.</li>
</ul>
<h2 id="schritt-5-kosten-nach-dichte-und-abbremsen"><a class="heading-anchor" href="#schritt-5-kosten-nach-dichte-und-abbremsen" aria-hidden="true" tabindex="-1">#</a>Schritt 5: Kosten nach Dichte und Abbremsen</h2>
<p>Auch mit einem glatten Feld nimmt die ganze Menge den kürzesten Weg eng um die Ecke, egal wie viele Gegner schon dort sind. Eine Lösung bietet <a href="https://grail.cs.washington.edu/projects/continuum-crowds" target="_blank" rel="noopener">Continuum Crowds</a> (Treuille et al., 2006): Die Kosten einer Zelle steigen mit der Dichte der Menge. Zählen Sie alle 10 Ticks die Gegner pro Zelle, glätten Sie das Ergebnis und berechnen Sie das Feld eines Ziels neu. An einer überfüllten Ecke schickt das Feld die Gegner dann außen herum, durch freiere Zellen.</p>
<figure class="post-compare"><div class="compare-grid"><div class="compare-item"><span class="compare-label">Ohne Dichte</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="Die Menge umgeht das Hindernis eng an der Ecke in einem schmalen Strom, die rote Dichtekarte ist konzentriert" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div><div class="compare-item"><span class="compare-label">Kosten nach Dichte</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="Die Menge umgeht das Hindernis in weitem Bogen, die Dichte ist verteilt" width="1792" height="1008" loading="lazy" decoding="async" /></picture></div></div><figcaption>Gleiche Menge um dieselbe Ecke. Rot ist die geglättete Dichte.</figcaption></figure>
<p>Die zweite Hälfte ist das Abbremsen. Ein Gegner in einer dichten Zelle läuft langsamer, minimal mit 15 % seiner Geschwindigkeit. Bei 2.250 Gegnern liefen sie in dünn besetzten Zellen im Schnitt mit 94 % ihrer Geschwindigkeit, bei mittlerer Dichte mit 71 % und im Stau mit 20 %. Lassen Sie das Abbremsen früh beginnen: Mit einer Schwelle für freie Dichte von 0,6 Gegnern pro Zelle statt 1 sank der Anteil der zu dicht stehenden in der Engstelle auf 36,7 %.</p>
<p>Ein Tipp aus der Praxis: Allokieren Sie Arrays, die erst zur Laufzeit entstehen (etwa die Dichtekarte), bevor ein Job sie zum ersten Mal nutzt. Ein Job mit einem noch nicht angelegten Array lässt den Unity Editor nativ abstürzen, ohne Exception.</p>
<h2 id="schritt-6-stau-in-der-engstelle-warten-statt-drucken"><a class="heading-anchor" href="#schritt-6-stau-in-der-engstelle-warten-statt-drucken" aria-hidden="true" tabindex="-1">#</a>Schritt 6: Stau in der Engstelle: warten statt drücken</h2>
<p>Wenn sich die Menge nirgends verteilen kann, bleibt nur die Engstelle. Die einfachste Idee ist, den Geschwindigkeitsanteil in Richtung eines berührten Nachbarn zu verwerfen. Die Hinteren bleiben dann stehen, statt auszuweichen, und halten den Stau am Leben.</p>
<p>Seitlich auszuweichen, etwa um 35° zur Seite mit weniger Nachbarn, ist besser, aber die Menge beginnt zu flackern. Eine geglättete Ausrichtung und eine niedrigere Drehgeschwindigkeit (180 statt 360 °/s) dämpfen nur das Symptom. Die Ursache liegt woanders: Bei ausgeglichener Dichte wählt der Gegner jeden Tick eine andere Seite. Hysterese hilft, also eine Bevorzugung der vorherigen Entscheidung: Die Seite aus dem letzten Tick bekommt beim Vergleich einen Bonus von 3 Nachbarn.</p>
<p>Die endgültige Lösung verbindet eine Kontaktbedingung mit drei Zuständen: gehen, ausweichen, warten. Der Geschwindigkeitsanteil in einen berührten Nachbarn wird verworfen, der seitliche bleibt. Wer weder eine freie Seite noch Platz vor sich hat, wartet und drückt nicht.</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">für jeden Gegner, jeden Tick:
    Geschwindigkeit = Richtung aus dem Flow Field × Tempo × Abbremsen nach Dichte
    Geschwindigkeit += Abstoßung von Nachbarn
    wenn vorne ein Nachbar berührt wird:
        wenn eine Seite frei ist: ausweichen (Seite mit Hysterese)
        sonst: warten
    Geschwindigkeitsanteil in berührte Nachbarn verwerfen
    bewegen, Anteil in die Wand verwerfen
danach für alle:
    überlappende Paare auseinanderschieben (halbe Überlappung, höchstens 0,15 m)
    von Wänden abstoßen</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="Menge vor einer Engstelle, weiße Punkte gehen, rote warten, hinter der Engstelle läuft eine Reihe mit Abständen" width="1792" height="1008" loading="lazy" decoding="async" /></picture><figcaption>Vor der Engstelle: Weiße gehen, rote warten. Dahinter läuft eine Reihe mit Abständen.</figcaption></figure>
<p>Achten Sie auf drei Dinge:</p>
<ul>
<li><strong>Geduld.</strong> Wenn sich zwei Gegner frontal begegnen, soll der mit der höheren ID warten. Eine Kette von Nachbarn kann ihn aber Tausende Ticks blockieren, deshalb versucht er nach einem Wartelimit ein Notausweichen.</li>
<li><strong>Nullgeschwindigkeit.</strong> Ein Gegner mit gewünschter Geschwindigkeit null verlässt den Zustand „warten“ nie und bleibt nach einer Welle auf freier Fläche stehen. Wer sich nicht bewegen will, sollte immer im Zustand „gehen“ sein.</li>
<li><strong>Schmaler Kegel.</strong> Suchen Sie den Nachbarn vorne in einem schmalen Kegel mit kurzer Sonde. Bei 45° schickte die Einstellung 55 bis 60 % der Menge auch auf freier Fläche ins Warten oder Ausweichen, bei 30° und kürzerer Sonde 41 %.</li>
</ul>
<h2 id="schritt-7-gro-e-gegner-uberholen"><a class="heading-anchor" href="#schritt-7-gro-e-gegner-uberholen" aria-hidden="true" tabindex="-1">#</a>Schritt 7: Große Gegner überholen</h2>
<p>Unterschiedlich große und schnelle Gegnertypen bringen weitere Fälle. Unser großer, langsamer Gegner hat in der Menge einen Radius von 1,25 m und läuft 1,1 m/s. Ein schneller Gegner dahinter konnte ihn nicht überholen: Die seitlichen Sonden schräg nach vorne waren immer näher am großen als die Summe der Radien, beide Seiten galten als belegt und der schnelle wartete. Die Lösung: Den Nachbarn, der gerade umgangen wird, berücksichtigt die Sonde nur auf seiner eigenen Seite. Ein Nachbar genau voraus blockiert keine Seite.</p>
<p>Der große Gegner wiederum stößt kleine aus seinem Weg zur Seite, in einem Sektor von ±45° vor sich. Stoßen Sie keine weg, die schneller in seine Richtung laufen, als er selbst will, sonst trifft es auch die, die ihn gerade überholen.</p>
<h2 id="schritt-8-slots-am-angriffsziel"><a class="heading-anchor" href="#schritt-8-slots-am-angriffsziel" aria-hidden="true" tabindex="-1">#</a>Schritt 8: Slots am Angriffsziel</h2>
<p>Wenn Gegner im Nahkampf angreifen, brauchen sie Platz um das Ziel. Unser Turm hat 24 Slots am Rand seiner Grundfläche von 5 × 5 m, jeder Gegner belegt einen und die anderen warten in einer Schlange.</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="Turm mit 24 Slots am Rand, die meisten belegt, ein gelber Weg führt im Bogen um den Turm zu einem freien Slot" width="1408" height="960" loading="lazy" decoding="async" /></picture><figcaption>Belegte Slots greifen an, freie sind gestrichelt. Ein neuer Gegner umrundet den Turm im Bogen zu einem freien Slot, hinter der vollen Seite wartet eine Schlange.</figcaption></figure>
<ul>
<li><strong>Im Kreis umrunden.</strong> Wenn Sie einen Slot nur anbieten, wenn eine gerade Linie zu ihm führt, bleiben die hinteren Slots leer, weil die Linie von vorne den Turm schneidet. Lassen Sie den Gegner den Turm im Bogen zu einem freien Slot umrunden.</li>
<li><strong>Kein Verdrängen.</strong> Verdrängen Sie keinen Gegner auf dem Weg zum Slot zugunsten eines anderen Anwärters. Das löst Ketten von Verdrängungen aus.</li>
<li><strong>Kein Abstoßen am Slot.</strong> Ein Gegner, der neben einem belegten Slot ankommt, wird vom Nachbarn weggestoßen und landet 13 cm neben seinem Ziel oder ganz daneben. Gegner im Slot und bis 0,5 m davon stoßen sich nicht gegenseitig ab.</li>
</ul>
<h2 id="performance-von-simulation-und-rendering"><a class="heading-anchor" href="#performance-von-simulation-und-rendering" aria-hidden="true" tabindex="-1">#</a>Performance von Simulation und Rendering</h2>
<p>Ein Benchmark im Unity Editor (M3 Pro, rund 9.600 einfache Gegner, zwei Engstellen) ergab 90 FPS bei 1440p und 100 bei 1080p. Die Auflösung ändert die FPS kaum, die Zeitbeschleunigung schon: Bei 10× fällt sie auf 20 FPS, weil pro Frame zehn Simulationsschritte laufen. Bei normaler Geschwindigkeit hat die Simulation Reserven.</p>
<p>Mit einem detaillierten Gegnermodell statt einer Kapsel verlagert sich der Engpass ins Rendering:</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Variante</th>
<th align="right">Gleichzeitig</th>
<th align="right">Dreiecke pro Modell</th>
<th align="right">Median FPS</th>
</tr>
</thead>
<tbody><tr>
<td>Kapsel</td>
<td align="right">2.612</td>
<td align="right">844</td>
<td align="right">197</td>
</tr>
<tr>
<td>Detailliertes Modell, mit Schatten</td>
<td align="right">2.976</td>
<td align="right">3.578</td>
<td align="right">48</td>
</tr>
<tr>
<td>Detailliertes Modell, ohne Schatten</td>
<td align="right">2.976</td>
<td align="right">3.578</td>
<td align="right">85</td>
</tr>
<tr>
<td>Vereinfachtes Modell, mit Schatten</td>
<td align="right">2.945</td>
<td align="right">878</td>
<td align="right">335</td>
</tr>
</tbody></table>
</div>
<p>In der Simulation lohnen sich drei Dinge:</p>
<ul>
<li><strong>Zwei Durchläufe über Nachbarn.</strong> Der günstige (Kontakte, Blockieren) läuft immer, der teure (seitliche Sonden, Dichte der Seiten) nur für blockierte Gegner, im freien Strom eine Minderheit. Vergleichen Sie Abstände im Quadrat, ohne Wurzel.</li>
<li><strong>Einfaches Sortieren.</strong> Synchrones Sortieren des Spatial Hash dauerte bei 3.000, 10.000, 20.000 und 50.000 Einträgen 0,37, 1,49, 3,16 und 8,46 ms, die Job-Variante 0,18, 0,93, 3,12 und 15,53 ms. Der Job ist nicht durchgehend schneller, einfaches Sortieren reicht.</li>
<li><strong>Zustands-Hash.</strong> Berechnen Sie bei Tick 600 und 1800 einen Hash über Positionen und Lebenspunkte. Ein Refactoring muss denselben Hash ergeben, eine Verhaltensänderung ändert ihn, und Sie können erklären, warum. Das funktioniert dank der deterministischen Reihenfolge im Spatial Hash.</li>
</ul>
<h2 id="grenzen-des-ansatzes"><a class="heading-anchor" href="#grenzen-des-ansatzes" aria-hidden="true" tabindex="-1">#</a>Grenzen des Ansatzes</h2>
<ul>
<li>Zwei volle Mengen in Gegenrichtung in einer Engstelle verkeilen sich dauerhaft. Das ist eine Grenze eines Modells ohne Physik.</li>
<li>Die Zahlen stammen aus dem Unity Editor auf einem M3 Pro. Messungen auf dem Steam Deck und in einem echten Build haben wir noch nicht, nur eine Schätzung aus öffentlichen CPU-Benchmarks.</li>
</ul>
<p>Wenn Sie eine eigene Menge ausprobieren wollen, ist die <a href="https://docs.unity3d.com/Packages/com.unity.entities@6.5/manual/" target="_blank" rel="noopener">Dokumentation von Unity Entities</a> ein guter Start. Einen ähnlichen Weg von der einfachsten zur funktionierenden Lösung beschreibt auch unser Artikel über den <a href="https://smallwater.eu/de/blog/sudoku-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/unity-dots-horde-navigation/" data-ga-destination="/de/blog/sudoku-generator/" data-ga-placement="inline">Sudoku-Generator</a>. Wie Sie Gegner in so einer Horde per Explosion wegschleudern, ohne für jeden einen teuren Ragdoll zu simulieren, zeigt die Anleitung zu <a href="https://smallwater.eu/de/blog/fake-ragdoll-unity-dots/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/unity-dots-horde-navigation/" data-ga-destination="/de/blog/fake-ragdoll-unity-dots/" data-ga-placement="inline">Fake-Ragdolls in Unity DOTS</a>. Wenn Sie an einer ähnlichen Simulation in Unity arbeiten und Hilfe möchten, <a href="https://smallwater.eu/de/#kontakt">schreiben Sie uns</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Wie man ein Wortsuchrätsel mit Lösungswort generiert</title>
      <link>https://smallwater.eu/de/blog/word-search-generator/</link>
      <guid isPermaLink="true">https://smallwater.eu/de/blog/word-search-generator/</guid>
      <pubDate>Sun, 01 Nov 2026 00:00:00 GMT</pubDate>
      <category>Kreuzworträtsel &amp; Puzzles</category>
      <description>Anleitung für einen Wortsuchrätsel-Generator mit Lösungswort: Wörterbuch, Wortlängen, Füllen des Gitters ohne Backtracking und Erfolgsquote je Gittergröße.</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="Wortsuchrätsel 10×10 mit farbig markierten Wörtern und dem Lösungswort in korallroten Feldern" width="1200" height="630" fetchpriority="high" decoding="async" /></picture><nav class="post-toc" aria-label="Inhalt"><p class="post-toc-title">Inhalt</p><ol><li><a href="#regeln-fur-das-gitter" data-ga="blog_toc_clicked" data-ga-heading-id="regeln-fur-das-gitter">Regeln für das Gitter</a></li><li><a href="#schritt-1-worterbuch-vorbereiten" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-1-worterbuch-vorbereiten">Schritt 1: Wörterbuch vorbereiten</a></li><li><a href="#schritt-2-losungswort-wahlen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-2-losungswort-wahlen">Schritt 2: Lösungswort wählen</a></li><li><a href="#warum-nicht-mit-dem-losungswort-beginnen" data-ga="blog_toc_clicked" data-ga-heading-id="warum-nicht-mit-dem-losungswort-beginnen">Warum nicht mit dem Lösungswort beginnen</a></li><li><a href="#schritt-3-gitter-fullen-bis-genau-n-locher-ubrig-sind" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-3-gitter-fullen-bis-genau-n-locher-ubrig-sind">Schritt 3: Gitter füllen, bis genau N Löcher übrig sind</a></li><li><a href="#schritt-4-wortlangen-mit-verstand-ziehen" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-4-wortlangen-mit-verstand-ziehen">Schritt 4: Wortlängen mit Verstand ziehen</a></li><li><a href="#schritt-5-losungswort-in-die-locher-gie-en" data-ga="blog_toc_clicked" data-ga-heading-id="schritt-5-losungswort-in-die-locher-gie-en">Schritt 5: Lösungswort in die Löcher gießen</a></li><li><a href="#wenn-ein-gitter-misslingt" data-ga="blog_toc_clicked" data-ga-heading-id="wenn-ein-gitter-misslingt">Wenn ein Gitter misslingt</a></li><li><a href="#erfolgsquote-und-tempo-je-gittergro-e" data-ga="blog_toc_clicked" data-ga-heading-id="erfolgsquote-und-tempo-je-gittergro-e">Erfolgsquote und Tempo je Gittergröße</a></li><li><a href="#tipp-dasselbe-gitter-bei-jedem-lauf" data-ga="blog_toc_clicked" data-ga-heading-id="tipp-dasselbe-gitter-bei-jedem-lauf">Tipp: dasselbe Gitter bei jedem Lauf</a></li></ol></nav><p>Ein Wortsuchrätsel mit Lösungswort generieren Sie so: Legen Sie zufällige Wörter aus dem Wörterbuch in ein leeres Gitter, bis genau so viele Felder frei bleiben, wie das Lösungswort Buchstaben hat. Diese Felder füllen Sie dann mit dem Lösungswort, von links nach rechts und von oben nach unten. Auf dem Handy dauert das ein paar Millisekunden und braucht kein Backtracking, also kein Zurückgehen zu früheren Entscheidungen, wenn der Generator feststeckt.</p>
<p>Ein Wortsuchrätsel ist ein Buchstabengitter, in dem man Wörter aus einer Liste sucht und streicht. Das Lösungswort ergibt sich aus den Buchstaben, die danach übrig bleiben. Der Generator muss also ein vollständig gefülltes Gitter bauen, in dem nach dem Streichen genau das Lösungswort stehen bleibt. Mit dem folgenden Ablauf entstehen die Wortsuchrätsel in unserer App mit Kreuzworträtseln und Wortsuche (<a href="https://smallwater.eu/crosswords-ios">App Store</a>, <a href="https://smallwater.eu/crosswords-android">Google Play</a>).</p>
<h2 id="regeln-fur-das-gitter"><a class="heading-anchor" href="#regeln-fur-das-gitter" aria-hidden="true" tabindex="-1">#</a>Regeln für das Gitter</h2>
<p>Legen Sie vorher fest, was ein fertiges Gitter erfüllen muss. In unserer App gilt:</p>
<ul>
<li>Wörter laufen in acht Richtungen: waagerecht, senkrecht und entlang beider Diagonalen, jeweils auch rückwärts.</li>
<li>Zwei Wörter dürfen sich höchstens einen Buchstaben teilen.</li>
<li>Kein Wort darf durch ein Feld des Lösungsworts laufen.</li>
<li>Das Gitter muss voll sein. Ein leeres Feld ist ein Fehler.</li>
<li>Das Lösungswort wird von links nach rechts und von oben nach unten aus allen nicht gestrichenen Buchstaben gelesen.</li>
</ul>
<h2 id="schritt-1-worterbuch-vorbereiten"><a class="heading-anchor" href="#schritt-1-worterbuch-vorbereiten" aria-hidden="true" tabindex="-1">#</a>Schritt 1: Wörterbuch vorbereiten</h2>
<p>Teilen Sie das Wörterbuch einmal am Anfang in Gruppen nach Wortlänge auf. Ein zufälliges Wort einer bestimmten Länge ist dann ein einziger Zufallsindex. Wenn Sie für jedes Wort das ganze Wörterbuch filtern, wird die Generierung spürbar langsamer, denn für ein Gitter werden Hunderte Wörter probiert.</p>
<p>Unser slowakisches Wörterbuch hat 16.713 Wörter mit 3 bis 21 Buchstaben, die meisten mit sieben oder acht, das deutsche 134.937. Ein paar praktische Tipps:</p>
<ul>
<li><strong>Lassen Sie Eigennamen und Abkürzungen im Wörterbuch.</strong> Dann tauchen im Gitter Wörter wie HOUSTON oder MAINE auf. Ohne kurze und „seltsame“ Wörter lässt sich das Gitter nicht dicht genug füllen.</li>
<li><strong>Filtern Sie Vulgäres mit einer eigenen Liste</strong>, die sich abschalten lässt, wenn jemand Inhalte für Erwachsene will.</li>
<li><strong>Bei mehreren Sprachen laden Sie nur die gerade gespielte.</strong> Das englische Wörterbuch hat über 414.000 Wörter. Werden beim App-Start alle vier Sprachen auf einmal geladen, steht ein schwächeres Handy bis zu 11 Sekunden still.</li>
</ul>
<h2 id="schritt-2-losungswort-wahlen"><a class="heading-anchor" href="#schritt-2-losungswort-wahlen" aria-hidden="true" tabindex="-1">#</a>Schritt 2: Lösungswort wählen</h2>
<p>Das Lösungswort sollte mindestens so viele Buchstaben haben wie die Gitterseite und höchstens vier mehr. Ein kürzeres geht im Gitter unter, ein längeres braucht zu viele Löcher.</p>
<p>In unserer App ist das Lösungswort die Pointe eines Witzes. Man streicht die Wörter und liest aus den übrigen Buchstaben, wie der Witz endet. Wir bauen das Lösungswort vom Ende des Witzes: Von hinten kommen ganze Wörter dazu, bis die Länge in diesem Bereich liegt.</p>
<p>Die Bilder unten verwenden den englischen Witz „Two snakes walk into a bar. Turns out they were lizards.“ (Zwei Schlangen gehen in eine Bar. Es stellt sich heraus, dass es Eidechsen waren.) auf einem 10×10-Gitter. Das Lösungswort ist WERELIZARDS, 11 Buchstaben. Die Wörter in den Bildern stammen aus dem englischen Wörterbuch, beschränkt auf geläufige Wörter, damit sie lesbar sind.</p>
<h2 id="warum-nicht-mit-dem-losungswort-beginnen"><a class="heading-anchor" href="#warum-nicht-mit-dem-losungswort-beginnen" aria-hidden="true" tabindex="-1">#</a>Warum nicht mit dem Lösungswort beginnen</h2>
<p>Die erste Idee ist, es wie mit dem Bleistift zu machen: die Buchstaben des Lösungsworts in Lesereihenfolge auf zufällige Felder verteilen und dann Wörter darum herum bauen.</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="Leeres 10×10-Gitter mit elf zufällig verteilten Buchstaben des Lösungsworts" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Die Buchstaben des Lösungsworts, verteilt noch vor den Wörtern.</figcaption></figure>
<p>Das sieht vernünftig aus, bis das Ende kommt. Der Generator wählt ein leeres Feld, versucht ein Wort in allen Richtungen und Verschiebungen hindurchzulegen, und wenn das Gitter dicht wird, bleiben isolierte Löcher. Ein einzelnes Loch zwischen zwei Buchstaben des Lösungsworts lässt sich mit nichts füllen, denn das kürzeste Wort hat drei Buchstaben und kein Wort darf durch das Lösungswort laufen.</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="Fast volles Gitter mit drei leeren Feldern, die kein Wort mehr füllen kann" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Nach 10.000 Versuchen bleiben drei Löcher, die kein Wort füllt.</figcaption></figure>
<p>Bei einer Stichprobe von 200 Gittern 10×10 ergab dieser Ansatz nur in 28 % der Fälle ein volles Gitter, im Schnitt blieben 1 bis 2 Löcher. Sie nachträglich zu flicken ist aufwendig. Setzen Sie das Lösungswort also erst ein, wenn die Wörter fertig sind.</p>
<h2 id="schritt-3-gitter-fullen-bis-genau-n-locher-ubrig-sind"><a class="heading-anchor" href="#schritt-3-gitter-fullen-bis-genau-n-locher-ubrig-sind" aria-hidden="true" tabindex="-1">#</a>Schritt 3: Gitter füllen, bis genau N Löcher übrig sind</h2>
<p>Das Füllen ist ein einfacher Greedy Algorithm: Jedes Wort wird gelegt, sobald es passt, und nie wieder entfernt. Der Generator merkt sich nur eine Zahl: wie viele Felder noch leer sind. Ein Wort legt er nur, wenn danach nicht weniger leere Felder übrig bleiben, als das Lösungswort Buchstaben hat (N).</p>
<pre class="code-block" data-lang="text"><code class="hljs language-plaintext">leer = Zeilen × Spalten
solange leer &gt; N und Versuche &lt; 2000:
    wort = zufälliges Wort der gewählten Länge
    höchstens 50-mal wiederholen:
        start = zufälliges Feld, richtung = zufällig aus 8
        wenn das Wort nach den Gitterregeln passt:
            neu = Anzahl leerer Felder, die das Wort füllt
            wenn leer - neu &gt;= N:
                Wort legen
                leer = leer - neu
                weiter mit dem nächsten Wort
wenn leer != N: Gitter wegwerfen und neu anfangen</code></pre>
<p>Die ersten Wörter landen frei im leeren Gitter:</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="Gitter mit den ersten drei Wörtern: eines waagerecht, zwei senkrecht" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Nach drei Wörtern. Jedes Wort hat seine eigene Farbe.</figcaption></figure>
<p>Mit der Zeit beginnen Wörter, Buchstaben zu teilen. Das ist erwünscht, denn jeder geteilte Buchstabe ist ein Feld weniger, das gefüllt werden muss:</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="Gitter mit dreizehn Wörtern, mehr als ein Drittel der Felder ist leer" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Dreizehn Wörter, 40 leere Felder.</figcaption></figure>
<p>Sobald die Zahl der leeren Felder auf N fällt, ist Schluss. Wo die Löcher liegen, ist egal. Sie dürfen überall verstreut sein, weil das Lösungswort keinen zusammenhängenden Weg bilden muss:</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="Gitter mit zweiundzwanzig Wörtern und elf leeren Feldern mit gestricheltem Rand" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Genau 11 leere Felder, so viele Buchstaben wie WERELIZARDS.</figcaption></figure>
<h2 id="schritt-4-wortlangen-mit-verstand-ziehen"><a class="heading-anchor" href="#schritt-4-wortlangen-mit-verstand-ziehen" aria-hidden="true" tabindex="-1">#</a>Schritt 4: Wortlängen mit Verstand ziehen</h2>
<p>Lange Wörter machen ein Wortsuchrätsel interessant, kurze machen es fertig. Ziehen Sie die Länge deshalb aus zwei Normalverteilungen (normalverteilte Zufallszahlen liefert zum Beispiel die <a href="https://en.wikipedia.org/wiki/Box%E2%80%93Muller_transform" target="_blank" rel="noopener">Box-Muller-Methode</a>):</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Wahrscheinlichkeit</th>
<th>Wortlänge</th>
</tr>
</thead>
<tbody><tr>
<td>50 %</td>
<td>etwa 6 Buchstaben, Standardabweichung 2</td>
</tr>
<tr>
<td>30 %</td>
<td>etwa 5 Buchstaben, Standardabweichung 1</td>
</tr>
<tr>
<td>20 %</td>
<td>3 Buchstaben</td>
</tr>
</tbody></table>
</div>
<p>Die Länge ist immer mindestens 3 und höchstens die Gitterseite. Dazu zwei Regeln, ohne die der Generator am Ende hängen bleibt:</p>
<ul>
<li><strong>Erzwingen Sie am Ende die genaue Länge.</strong> Fehlen 5 oder weniger Felder, suchen Sie ein Wort mit genau der fehlenden Länge (mindestens 3). Sonst versucht der Generator endlos Sechs-Buchstaben-Wörter, die nicht mehr passen.</li>
<li><strong>Rationieren Sie Wörter mit drei Buchstaben.</strong> Die ersten drei Versuche an einem Gitter dürfen keins enthalten, Versuche 4 bis 6 eins, die nächsten drei zwei und so weiter. Drei Buchstaben sind im Wörterbuch oft Abkürzungen wie DNA oder CPU und wirken im Rätsel billig. Erlauben Sie sie erst, wenn es anders nicht geht.</li>
</ul>
<h2 id="schritt-5-losungswort-in-die-locher-gie-en"><a class="heading-anchor" href="#schritt-5-losungswort-in-die-locher-gie-en" aria-hidden="true" tabindex="-1">#</a>Schritt 5: Lösungswort in die Löcher gießen</h2>
<p>Gehen Sie das Gitter von links nach rechts und von oben nach unten durch und schreiben Sie in jedes leere Feld den nächsten Buchstaben des Lösungsworts. Da es genau N Löcher gibt, passt das Lösungswort ohne Rest.</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="Dasselbe Gitter, leere Felder mit den Buchstaben des Lösungsworts gefüllt und von 1 bis 11 nummeriert" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Buchstaben des Lösungsworts in Lesereihenfolge. Man findet sie als letzte nicht gestrichene Buchstaben.</figcaption></figure>
<p>Zu sehen ist am Ende nur das:</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="Fertiges 10×10-Wortsuchrätsel ohne Markierungen" width="976" height="986" loading="lazy" decoding="async" /></picture><figcaption>Das fertige Gitter, wie es im Spiel erscheint.</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">Ausprobieren</p><h2 class="app-box-title">Kreuzworträtsel &amp; Puzzles</h2><p>Kreuzworträtsel, Wortsuche, Sudoku und Nonogramme in einer Offline-App. Wie die Rätsel entstehen und was neu ist.</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="wenn-ein-gitter-misslingt"><a class="heading-anchor" href="#wenn-ein-gitter-misslingt" aria-hidden="true" tabindex="-1">#</a>Wenn ein Gitter misslingt</h2>
<p>Erreicht der Generator innerhalb von 2.000 Versuchen nicht genau N leere Felder, werfen Sie das ganze Gitter weg und fangen neu an, höchstens 100-mal. Kein Flicken von Löchern. Das ist grob, aber für Gitter bis 12×12 reicht es mit Abstand. Größere Gitter brauchen mehr Versuche, wie die Tabelle unten zeigt.</p>
<h2 id="erfolgsquote-und-tempo-je-gittergro-e"><a class="heading-anchor" href="#erfolgsquote-und-tempo-je-gittergro-e" aria-hidden="true" tabindex="-1">#</a>Erfolgsquote und Tempo je Gittergröße</h2>
<p>Der Generator lief auf 300 Gittern pro Größe, mit echten Witzen und dem slowakischen Wörterbuch (Node.js 22 auf einer Server-CPU, ein Handy ist langsamer; die Bilder nutzen das englische Wörterbuch, die Zahlen das slowakische, weil es kleiner und damit der strengere Test ist). „Erster Versuch“ heißt, dass die erste Füllrunde mit einem vollen Gitter endete.</p>
<div class="table-wrap"><table>
<thead>
<tr>
<th>Gitter</th>
<th align="right">Erster Versuch</th>
<th align="right">Ø Versuche</th>
<th align="right">Median Zeit</th>
<th align="right">Wörter im Gitter</th>
<th align="right">Anteil Diagonalen</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>Was das für einen eigenen Generator bedeutet:</p>
<ul>
<li><strong>Das durchschnittliche Wort hat 4,7 Buchstaben</strong>, obwohl das Ziel bei etwa 6 liegt. Lange Wörter passen in ein dichtes Gitter einfach öfter nicht.</li>
<li><strong>Etwa 9 bis 10 % der Felder teilen sich zwei Wörter.</strong> Das müssen Sie nicht einstellen, es folgt aus der Regel „höchstens ein gemeinsamer Buchstabe“.</li>
<li><strong>Es gibt weniger Diagonalen, als Sie erwarten.</strong> Die Richtung wird gleichverteilt aus acht gezogen, Diagonalen müssten also die Hälfte der Wörter sein. In einem kleinen Gitter fällt ein diagonales Wort aber viel öfter über den Rand. Bei 8×8 sind 24 % der Wörter diagonal, bei 15×15 schon 33 %. Wenn Sie mehr Diagonalen wollen, geben Sie ihnen beim Ziehen der Richtung mehr Gewicht.</li>
</ul>
<h2 id="tipp-dasselbe-gitter-bei-jedem-lauf"><a class="heading-anchor" href="#tipp-dasselbe-gitter-bei-jedem-lauf" aria-hidden="true" tabindex="-1">#</a>Tipp: dasselbe Gitter bei jedem Lauf</h2>
<p>Für Store-Screenshots brauchen Sie jedes Mal genau dasselbe Gitter. Ersetzen Sie in diesem Modus den üblichen Zufallsgenerator durch einen einfachen linearen Kongruenzgenerator mit festem Seed (zum Beispiel aus der Sprache abgeleitet). Bleibt die gesamte Arithmetik in dem Bereich, den Zahlen in JavaScript exakt darstellen, ist das Ergebnis auf jedem Gerät gleich. Mit demselben Trick sind auch die Bilder in diesem Artikel entstanden.</p>
<p>Wenn Sie ein ähnliches Problem lösen, Rätsel generieren oder eine mobile App brauchen, <a href="https://smallwater.eu/de/#kontakt">schreiben Sie uns</a>. Kurze Clips aus der Entwicklung gibt es auch auf <a href="https://www.youtube.com/@smallwaterinc" target="_blank" rel="noopener">YouTube</a>. Ebenso Schritt für Schritt ist beschrieben, <a href="https://smallwater.eu/de/blog/sudoku-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/word-search-generator/" data-ga-destination="/de/blog/sudoku-generator/" data-ga-placement="inline">wie man ein Sudoku mit eindeutiger Lösung generiert</a>. Für Kreuzworträtsel statt Wortsuche lesen Sie, <a href="https://smallwater.eu/de/blog/swedish-crossword-generator/" data-ga="blog_related_link_clicked" data-ga-source-path="/de/blog/word-search-generator/" data-ga-destination="/de/blog/swedish-crossword-generator/" data-ga-placement="inline">wie man ein Schwedenrätsel mit Lösungswort generiert</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
