Happy_Forest/Docs/CodeDokumentation.html
2026-08-04 20:56:53 +02:00

2353 lines
125 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<title>Happy Forest — Code-Dokumentation &amp; Editor-Anleitung</title>
<meta name="description" content="Zeile-für-Zeile-Erklärung aller Scripts (Pille, Skirmish, Base/Placement) plus Editor-Setup-Anleitung, mit Verständnis-Checkboxen." />
<style>
:root {
--bg: #EDEFF2;
--surface: #FFFFFF;
--surface-2: #F5F6F8;
--text: #1B222C;
--text-dim: #565F6D;
--border: #D6DAE1;
--accent: #C9812E;
--accent-text: #7A4A15;
--pille: #3F8577;
--pille-bg: #E3F1EE;
--sk: #B14A3B;
--sk-bg: #FBE9E5;
--baseui: #4C6FB1;
--baseui-bg: #E7ECF7;
--ok: #3F8577;
--code-bg: #F7F5F0;
--code-border: #E4DFD3;
--shadow: 0 1px 2px rgba(20, 24, 30, 0.06), 0 4px 14px rgba(20, 24, 30, 0.05);
--font-ui: ui-sans-serif, -apple-system, "Segoe UI", Roboto, Arial, sans-serif;
--font-mono: ui-monospace, "Cascadia Code", "SF Mono", Consolas, "Liberation Mono", monospace;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #10141A;
--surface: #1A2028;
--surface-2: #1F2630;
--text: #E7EAEF;
--text-dim: #94A0AF;
--border: #2B333F;
--accent: #E0A254;
--accent-text: #F0C185;
--pille: #6BB6A4;
--pille-bg: #16302A;
--sk: #D9776A;
--sk-bg: #351E1B;
--baseui: #8CA3DE;
--baseui-bg: #1D2740;
--code-bg: #161B22;
--code-border: #2A313C;
--shadow: 0 1px 2px rgba(0,0,0,0.3), 0 8px 24px rgba(0,0,0,0.35);
}
}
:root[data-theme="dark"] {
--bg: #10141A; --surface: #1A2028; --surface-2: #1F2630; --text: #E7EAEF; --text-dim: #94A0AF;
--border: #2B333F; --accent: #E0A254; --accent-text: #F0C185; --pille: #6BB6A4; --pille-bg: #16302A;
--sk: #D9776A; --sk-bg: #351E1B; --baseui: #8CA3DE; --baseui-bg: #1D2740; --code-bg: #161B22; --code-border: #2A313C;
--shadow: 0 1px 2px rgba(0,0,0,0.3), 0 8px 24px rgba(0,0,0,0.35);
}
:root[data-theme="light"] {
--bg: #EDEFF2; --surface: #FFFFFF; --surface-2: #F5F6F8; --text: #1B222C; --text-dim: #565F6D;
--border: #D6DAE1; --accent: #C9812E; --accent-text: #7A4A15; --pille: #3F8577; --pille-bg: #E3F1EE;
--sk: #B14A3B; --sk-bg: #FBE9E5; --baseui: #4C6FB1; --baseui-bg: #E7ECF7; --code-bg: #F7F5F0; --code-border: #E4DFD3;
--shadow: 0 1px 2px rgba(20, 24, 30, 0.06), 0 4px 14px rgba(20, 24, 30, 0.05);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-ui);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
a { color: var(--accent-text); }
.layout {
display: grid;
grid-template-columns: 260px minmax(0, 1fr);
gap: 0;
max-width: 1240px;
margin: 0 auto;
}
.sidebar {
position: sticky;
top: 0;
align-self: start;
height: 100vh;
overflow-y: auto;
padding: 20px 14px 40px;
border-right: 1px solid var(--border);
}
.sidebar h1 {
font-size: 0.95rem;
letter-spacing: 0.02em;
margin: 4px 6px 2px;
}
.sidebar .sub {
font-size: 0.78rem;
color: var(--text-dim);
margin: 0 6px 16px;
}
.progress-box {
margin: 0 6px 18px;
padding: 10px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
box-shadow: var(--shadow);
}
.progress-bar {
height: 6px;
background: var(--surface-2);
border-radius: 4px;
overflow: hidden;
margin-top: 6px;
}
.progress-fill {
height: 100%;
width: 0%;
background: var(--accent);
transition: width 0.2s ease;
}
#progress-text {
font-size: 0.75rem;
color: var(--text-dim);
font-variant-numeric: tabular-nums;
}
.nav-group { margin-bottom: 6px; }
.nav-group-label {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-dim);
padding: 10px 8px 4px;
}
.nav-link {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 6px;
color: var(--text);
text-decoration: none;
font-size: 0.84rem;
font-family: var(--font-mono);
}
.nav-link:hover { background: var(--surface-2); }
.nav-dot {
width: 7px; height: 7px; border-radius: 50%; flex: none;
}
.nav-dot.pille { background: var(--pille); }
.nav-dot.sk { background: var(--sk); }
.nav-dot.baseui { background: var(--baseui); }
.nav-dot.setup { background: var(--accent); }
.nav-count {
margin-left: auto;
font-size: 0.7rem;
color: var(--text-dim);
font-variant-numeric: tabular-nums;
}
main {
padding: 40px clamp(20px, 4vw, 56px) 120px;
min-width: 0;
}
.intro {
max-width: 680px;
margin-bottom: 40px;
}
.intro h1 {
font-size: clamp(1.6rem, 2.4vw, 2.1rem);
text-wrap: balance;
margin: 0 0 10px;
}
.intro p {
color: var(--text-dim);
max-width: 62ch;
}
section.system-header {
margin: 56px 0 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--border);
}
section.system-header h2 {
font-size: 1.5rem;
margin: 0 0 4px;
}
section.system-header p { color: var(--text-dim); margin: 0; max-width: 68ch; }
.file-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
box-shadow: var(--shadow);
margin: 22px 0;
overflow: hidden;
scroll-margin-top: 16px;
}
.file-card.pille { border-left: 4px solid var(--pille); }
.file-card.sk { border-left: 4px solid var(--sk); }
.file-card.baseui { border-left: 4px solid var(--baseui); }
.file-card.setup { border-left: 4px solid var(--accent); }
.file-head {
padding: 16px 20px 12px;
border-bottom: 1px solid var(--border);
}
.file-tag {
display: inline-block;
font-family: var(--font-mono);
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 2px 8px;
border-radius: 4px;
margin-bottom: 8px;
}
.file-tag.pille { background: var(--pille-bg); color: var(--pille); }
.file-tag.sk { background: var(--sk-bg); color: var(--sk); }
.file-tag.baseui { background: var(--baseui-bg); color: var(--baseui); }
.file-head h3 {
font-family: var(--font-mono);
font-size: 1.05rem;
margin: 0 0 6px;
}
.file-head .purpose { color: var(--text-dim); font-size: 0.92rem; max-width: 68ch; }
.item-list { list-style: none; margin: 0; padding: 10px 12px 16px; }
.item {
display: grid;
grid-template-columns: 24px minmax(0, 1fr);
gap: 4px 10px;
padding: 10px 8px;
border-radius: 8px;
}
.item:hover { background: var(--surface-2); }
.item input[type="checkbox"] {
width: 18px; height: 18px;
margin-top: 3px;
accent-color: var(--accent);
cursor: pointer;
}
.item input[type="checkbox"]:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.item-body { min-width: 0; }
.item.checked .item-body { opacity: 0.55; }
.ln {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--text-dim);
background: var(--surface-2);
border: 1px solid var(--border);
padding: 1px 6px;
border-radius: 4px;
display: inline-block;
margin-bottom: 6px;
}
.code {
display: block;
overflow-x: auto;
font-family: var(--font-mono);
font-size: 0.82rem;
background: var(--code-bg);
border: 1px solid var(--code-border);
border-radius: 6px;
padding: 8px 10px;
margin: 0 0 8px;
white-space: pre;
color: var(--text);
}
.explain { margin: 0; font-size: 0.92rem; }
.explain code {
font-family: var(--font-mono);
background: var(--surface-2);
padding: 1px 5px;
border-radius: 4px;
font-size: 0.86rem;
}
.steps { list-style: none; margin: 0; padding: 10px 12px 16px; }
.step {
display: grid;
grid-template-columns: 24px minmax(0,1fr);
gap: 4px 10px;
padding: 10px 8px;
border-radius: 8px;
}
.step:hover { background: var(--surface-2); }
.step input[type="checkbox"] { width: 18px; height: 18px; margin-top: 3px; accent-color: var(--accent); cursor: pointer; }
.step.checked .step-body { opacity: 0.55; }
.step-body p { margin: 4px 0 0; font-size: 0.88rem; color: var(--text-dim); }
.step-body strong { color: var(--text); }
.badge-done {
display: inline-block;
font-family: var(--font-mono);
font-size: 0.68rem;
background: var(--pille-bg);
color: var(--pille);
padding: 1px 7px;
border-radius: 4px;
margin-left: 6px;
}
table.keys {
border-collapse: collapse;
width: 100%;
margin: 8px 0 4px;
font-size: 0.86rem;
}
table.keys th, table.keys td {
text-align: left;
padding: 6px 10px;
border-bottom: 1px solid var(--border);
}
table.keys th { color: var(--text-dim); font-weight: 600; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.04em; }
table.keys td.key { font-family: var(--font-mono); }
.callout {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 12px 14px;
margin: 12px 12px 16px;
font-size: 0.88rem;
}
.callout strong { color: var(--accent-text); }
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; }
.sidebar { position: static; height: auto; border-right: none; border-bottom: 1px solid var(--border); }
}
</style>
<div class="layout">
<nav class="sidebar">
<h1>Code-Dokumentation</h1>
<p class="sub">Happy Forest — DOTS-Referenz</p>
<div class="progress-box">
<div id="progress-text">0 / 0 verstanden (0%)</div>
<div class="progress-bar"><div class="progress-fill" id="progress-fill"></div></div>
</div>
<div class="nav-group">
<div class="nav-group-label">Setup</div>
<a class="nav-link" href="#editor-setup"><span class="nav-dot setup"></span>Editor-Anleitung</a>
</div>
<div class="nav-group">
<div class="nav-group-label">System: Pille</div>
<a class="nav-link" href="#f-pille-component"><span class="nav-dot pille"></span>PilleComponent.cs</a>
<a class="nav-link" href="#f-pille-config"><span class="nav-dot pille"></span>PilleSimulationConfig.cs</a>
<a class="nav-link" href="#f-pille-movement"><span class="nav-dot pille"></span>PilleMovementSystem.cs</a>
<a class="nav-link" href="#f-pille-collision"><span class="nav-dot pille"></span>PilleCollisionSystem.cs</a>
<a class="nav-link" href="#f-pille-bootstrap"><span class="nav-dot pille"></span>PilleDotsBootstrap.cs</a>
</div>
<div class="nav-group">
<div class="nav-group-label">System: Skirmish</div>
<a class="nav-link" href="#f-sk-unitdef"><span class="nav-dot sk"></span>UnitDefinition.cs</a>
<a class="nav-link" href="#f-sk-unitcomp"><span class="nav-dot sk"></span>UnitComponents.cs</a>
<a class="nav-link" href="#f-sk-movecomp"><span class="nav-dot sk"></span>MovementComponents.cs</a>
<a class="nav-link" href="#f-sk-lanemove"><span class="nav-dot sk"></span>LaneMovementSystem.cs</a>
<a class="nav-link" href="#f-sk-grid"><span class="nav-dot sk"></span>BuildLaneGridSystem.cs</a>
<a class="nav-link" href="#f-sk-targeting"><span class="nav-dot sk"></span>TargetingSystem.cs</a>
<a class="nav-link" href="#f-sk-combat"><span class="nav-dot sk"></span>CombatSystem.cs</a>
<a class="nav-link" href="#f-sk-death"><span class="nav-dot sk"></span>DeathSystem.cs</a>
<a class="nav-link" href="#f-sk-bootstrap"><span class="nav-dot sk"></span>SkirmishBootstrap.cs</a>
</div>
<div class="nav-group">
<div class="nav-group-label">System: Base / Placement</div>
<a class="nav-link" href="#f-base-economy"><span class="nav-dot baseui"></span>PlayerEconomy.cs</a>
<a class="nav-link" href="#f-base-grid"><span class="nav-dot baseui"></span>BaseGrid.cs</a>
<a class="nav-link" href="#f-base-shopui"><span class="nav-dot baseui"></span>ShopUI.cs</a>
<a class="nav-link" href="#f-base-placement"><span class="nav-dot baseui"></span>PlacementController.cs</a>
<a class="nav-link" href="#f-base-wavemanager"><span class="nav-dot baseui"></span>WaveManager.cs</a>
</div>
</nav>
<main>
<div class="intro">
<h1>Was hier passiert, Zeile für Zeile</h1>
<p>Diese Seite erklärt jede Datei im Projekt: erst wofür sie da ist, dann jede Zeile/jeden Block einzeln. Häkchen setzen = verstanden. Der Haken bleibt gespeichert (im Browser dieses Geräts), auch nach Neuladen.</p>
</div>
<!-- ====================== EDITOR SETUP ====================== -->
<section id="editor-setup" class="file-card setup">
<div class="file-head">
<span class="file-tag" style="background:var(--surface-2); color:var(--accent-text);">Editor-Setup</span>
<h3>Was du in Unity einrichten musst</h3>
<div class="purpose">Reihenfolge: erst prüfen, was schon steht (Pille lief ja schon), dann Skirmish aufsetzen.</div>
</div>
<div class="callout">
<strong>Bereits erledigt</strong> — bestätigt durch deine eigenen Screenshots (Pille rendert, 200.000 Stück liefen). Entities Graphics (1.4.21), URP (17.3.0) sind installiert, das URP-Asset ist zugewiesen, Materialien sind konvertiert. Hier nur zur Erinnerung gelistet, falls du sie in einem neuen Projekt nochmal brauchst.
</div>
<ul class="steps">
<li class="step">
<input type="checkbox" data-key="setup-1" />
<div class="step-body">
<strong>Entities Graphics Paket <span class="badge-done">erledigt</span></strong>
<p>Window → Package Manager → Unity Registry → "Entities Graphics" installieren. Ohne dieses Paket können DOTS-Entities gar kein Mesh/Material rendern.</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-2" />
<div class="step-body">
<strong>URP installiert + zugewiesen <span class="badge-done">erledigt</span></strong>
<p>Universal RP Paket, ein URP-Asset erstellt (Assets → Create → Rendering → URP Asset) und in Edit → Project Settings → Graphics (+ Quality-Tab) eingetragen. Entities Graphics funktioniert NUR mit URP oder HDRP, nicht mit Built-in RP — das war der Grund, warum die ersten Pillen unsichtbar waren.</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-3" />
<div class="step-body">
<strong>Materialien auf URP konvertiert <span class="badge-done">erledigt</span></strong>
<p>Window → Rendering → Render Pipeline Converter, "Built-in to URP" → Convert Assets. Ohne das sehen Materialien pink aus (Standard-Shader existiert unter URP nicht).</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-4" />
<div class="step-body">
<strong>Pille-Setup <span class="badge-done">erledigt</span></strong>
<p>Ein leeres GameObject mit <code>PilleDotsBootstrap.cs</code>, Mesh + Material im Inspector zugewiesen, vier Wände in der Szene mit Tag <code>Wall</code>.</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-5" />
<div class="step-body">
<strong>NEU — Unit-Definitionen für Skirmish anlegen</strong>
<p>Rechtsklick im Project-Fenster (z.B. in einem neuen Ordner <code>Assets/SkirmishData</code>) → <strong>Create → Skirmish → Unit Definition</strong>. Mindestens 1, besser 2 Stück (z.B. "Grunt" für Nahkampf, "Archer" für Fernkampf). Im Inspector: Werte eintragen (Leben, Tempo, Schaden, Reichweite, Cooldown), Mesh (z.B. Capsule) und Material zuweisen.</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-6" />
<div class="step-body">
<strong>NEU — SkirmishBootstrap-GameObject</strong>
<p>Neues leeres GameObject in derselben Szene (kann neben dem Pille-GameObject existieren) → <code>SkirmishBootstrap.cs</code> draufziehen. Im Inspector das <strong>Unit-Typen</strong>-Array mit deinen Unit-Definitionen befüllen.</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-7" />
<div class="step-body">
<strong>Pille/Skirmish-Test durchführen</strong>
<p>Siehe Tastenbelegung unten. Falls beim ersten Compile ein Fehler kommt: melden, kann ich hier nicht selbst kompilieren/testen.</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-8" />
<div class="step-body">
<strong>NEU — Base/Placement: Für jedes Team ein Grid</strong>
<p>Neues leeres GameObject (z.B. "Team0_Base") irgendwo neben der Lane in der Szene positionieren, <code>BaseGrid.cs</code> draufziehen, im Inspector Spalten/Reihen/Zellgröße einstellen. Im Scene-View zeigt ein gelbes Gizmo-Raster, wo das Grid tatsächlich liegt — daran ausrichten. Dasselbe für Team 1 an der anderen Seite der Lane wiederholen (zwei separate GameObjects, zwei separate BaseGrid-Komponenten).</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-9" />
<div class="step-body">
<strong>NEU — Economy pro Team</strong>
<p>Auf jedem der beiden Base-GameObjects zusätzlich <code>PlayerEconomy.cs</code> draufziehen (Startgold/Wellen-Einkommen im Inspector einstellbar).</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-10" />
<div class="step-body">
<strong>NEU — Shop + Platzierung pro Team</strong>
<p>Je ein weiteres GameObject (z.B. "Team0_Shop") mit <code>ShopUI.cs</code> UND <code>PlacementController.cs</code>. Im Inspector verdrahten: <code>baseGrid</code> → das passende Team-Grid, <code>economy</code> → die passende PlayerEconomy, bei ShopUI zusätzlich <code>placementController</code> → das PlacementController-Component auf demselben Objekt, bei PlacementController zusätzlich <code>shopUI</code> → die ShopUI-Component. Beide brauchen dasselbe <code>shopUnits</code>-Array wie <code>SkirmishBootstrap</code><strong>gleiche Reihenfolge!</strong> Der Index in diesem Array ist gleichzeitig die UnitTypeId.</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-11" />
<div class="step-body">
<strong>NEU — WaveManager</strong>
<p>Ein GameObject mit <code>WaveManager.cs</code>. Im Inspector: <code>bootstrap</code> → das SkirmishBootstrap-GameObject, <code>team0Grid</code>/<code>team1Grid</code> → die beiden BaseGrids, <code>team0Economy</code>/<code>team1Economy</code> → die beiden PlayerEconomy-Komponenten, <code>playerCount</code> nach Bedarf (bestimmt das Wellen-Intervall zwischen 2040s).</p>
</div>
</li>
<li class="step">
<input type="checkbox" data-key="setup-12" />
<div class="step-body">
<strong>NEU — Base/Placement testen</strong>
<p>Play drücken, im Shop-Balken unten eine Unit anklicken (wird markiert), dann auf das gelbe Grid im 3D-Fenster klicken → Vorschau-Mesh erscheint, Gold wird abgezogen. Warten, bis der Timer oben ("Nächste Welle in: ...") abläuft → platzierte Units erscheinen automatisch am Lane-Eingang und laufen los.</p>
</div>
</li>
</ul>
<div style="padding: 4px 20px 20px;">
<table class="keys">
<thead><tr><th>Taste</th><th>System</th><th>Wirkung</th></tr></thead>
<tbody>
<tr><td class="key">Space</td><td>Pille</td><td>1 Pille spawnen</td></tr>
<tr><td class="key">M</td><td>Pille</td><td>1 Pille löschen</td></tr>
<tr><td class="key">C</td><td>Pille</td><td>100 Pillen spawnen</td></tr>
<tr><td class="key">K</td><td>Pille</td><td>Pille-gegen-Pille-Kollision an/aus</td></tr>
<tr><td class="key">1</td><td>Skirmish</td><td>1 Unit für Team A spawnen</td></tr>
<tr><td class="key">2</td><td>Skirmish</td><td>1 Unit für Team B spawnen</td></tr>
<tr><td class="key">3</td><td>Skirmish</td><td>Test-Welle (20 Units je Team, zufällig gemischt)</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ====================== SYSTEM: PILLE ====================== -->
<section class="system-header">
<h2>System 1 — Pille (Stresstest-Prototyp)</h2>
<p>Der erste DOTS-Test: möglichst viele Entities gleichzeitig bewegen, um zu sehen, wo die Grenze liegt (Ergebnis: 200.000 bei 61 FPS). 5 Dateien.</p>
</section>
<article id="f-pille-component" class="file-card pille">
<div class="file-head">
<span class="file-tag pille">Komponente (Daten)</span>
<h3>PilleComponent.cs</h3>
<div class="purpose">Definiert, welche Daten eine einzelne Pille im Speicher hat — sonst nichts, keine Logik.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="pc-1" />
<div class="item-body">
<span class="ln">Z. 12</span>
<pre class="code">using Unity.Entities;
using Unity.Mathematics;</pre>
<p class="explain"><code>Unity.Entities</code> bringt das ECS-Grundgerüst mit (<code>IComponentData</code>, <code>Entity</code>, <code>EntityManager</code> ...). <code>Unity.Mathematics</code> bringt <code>float3</code> — einen Vektor mit x/y/z, der Burst-kompatibel ist (im Gegensatz zu <code>UnityEngine.Vector3</code>, das für den High-Speed-Compiler ungeeignet ist).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pc-2" />
<div class="item-body">
<span class="ln">Z. 4</span>
<pre class="code">namespace HappyForest.Prototypen</pre>
<p class="explain">Ein "Namensraum" — bündelt allen Pille-Code unter einem Namen, damit z.B. <code>Team</code> bei Skirmish nicht mit etwas Gleichnamigem hier kollidiert.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pc-3" />
<div class="item-body">
<span class="ln">Z. 6</span>
<pre class="code">public struct PilleComponent : IComponentData</pre>
<p class="explain">Eine ECS-"Komponente": reine Daten, kein Verhalten. <code>struct</code> statt <code>class</code> ist Pflicht — ECS-Komponenten müssen "unmanaged" (blittable) sein, damit Burst sie im rohen Speicher extrem schnell verarbeiten kann. <code>IComponentData</code> ist ein leeres Marker-Interface: es sagt Unity "das hier darf an eine Entity gehängt werden".</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pc-4" />
<div class="item-body">
<span class="ln">Z. 8</span>
<pre class="code">public float3 Velocity;</pre>
<p class="explain">Die aktuelle Bewegungsrichtung als Einheitsvektor (Länge ≈ 1), z.B. <code>(1,0,0)</code> = nach rechts.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pc-5" />
<div class="item-body">
<span class="ln">Z. 9</span>
<pre class="code">public float Speed;</pre>
<p class="explain">Wie schnell sich die Pille bewegt (Einheiten pro Sekunde) — unabhängig von der Richtung.</p>
</div>
</li>
</ul>
</article>
<article id="f-pille-config" class="file-card pille">
<div class="file-head">
<span class="file-tag pille">Komponente (Singleton)</span>
<h3>PilleSimulationConfig.cs</h3>
<div class="purpose">Ein globaler Einstellungs-Container. Es gibt genau eine Entity mit dieser Komponente — alle Systeme lesen von hier.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="pcfg-1" />
<div class="item-body">
<span class="ln">Z. 12</span>
<pre class="code">using Unity.Entities;
using Unity.Mathematics;</pre>
<p class="explain">Gleich wie bei <code>PilleComponent.cs</code>.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcfg-2" />
<div class="item-body">
<span class="ln">Z. 67 (Kommentar)</span>
<pre class="code">// Singleton-Komponente: Wird einmal beim Start von PilleDotsBootstrap erzeugt
// und steuert das Verhalten aller Systeme (Grenzen, Kollisions-Toggle etc.).</pre>
<p class="explain">"Singleton" heißt: von dieser Komponente existiert immer nur <em>eine</em> Entity in der ganzen Welt — wie eine globale Variable, nur ECS-tauglich.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcfg-3" />
<div class="item-body">
<span class="ln">Z. 8</span>
<pre class="code">public struct PilleSimulationConfig : IComponentData</pre>
<p class="explain">Wie zuvor: struct + <code>IComponentData</code> = ECS-Komponente.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcfg-4" />
<div class="item-body">
<span class="ln">Z. 1011</span>
<pre class="code">public float3 BoundsMin;
public float3 BoundsMax;</pre>
<p class="explain">Die minimale/maximale erlaubte Position im Feld (x, y, z) — berechnet aus den "Wall"-Objekten in der Szene.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcfg-5" />
<div class="item-body">
<span class="ln">Z. 12</span>
<pre class="code">public float PlayHeight;</pre>
<p class="explain">Die feste Höhe (Y), auf der alle Pillen bleiben — sie fallen/fliegen nie.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcfg-6" />
<div class="item-body">
<span class="ln">Z. 13</span>
<pre class="code">public float PilleRadius;</pre>
<p class="explain">Der "Radius" einer Pille — Grundlage für die Kollisionsberechnung.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcfg-7" />
<div class="item-body">
<span class="ln">Z. 14</span>
<pre class="code">public bool PilleCollisionEnabled;</pre>
<p class="explain">Der Ein/Aus-Schalter, den die K-Taste umschaltet.</p>
</div>
</li>
</ul>
</article>
<article id="f-pille-movement" class="file-card pille">
<div class="file-head">
<span class="file-tag pille">System (Burst)</span>
<h3>PilleMovementSystem.cs</h3>
<div class="purpose">Bewegt jeden Frame alle Pillen und lässt sie an den Feld-Grenzen abprallen — komplett Burst-kompiliert, läuft parallel über alle CPU-Kerne.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="pms-1" />
<div class="item-body">
<span class="ln">Z. 14</span>
<pre class="code">using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;</pre>
<p class="explain"><code>Unity.Burst</code> bringt <code>[BurstCompile]</code> — Unitys Compiler, der C#-Code in echten Maschinencode übersetzt statt ihn normal zu interpretieren. Das ist der Haupttreiber der 200.000-Pillen-Performance. <code>Unity.Transforms</code> bringt <code>LocalTransform</code> — die ECS-Version von Position/Rotation/Scale (Pendant zu <code>Transform</code> bei normalen GameObjects).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-2" />
<div class="item-body">
<span class="ln">Z. 1011</span>
<pre class="code">[BurstCompile]
public partial struct PilleMovementSystem : ISystem</pre>
<p class="explain"><code>ISystem</code> ist das Interface für ein "System" — der Ort, wo Logik passiert (im Gegensatz zu Komponenten, die nur Daten sind). <code>partial</code>, weil Unity im Hintergrund per Code-Generator zusätzlichen Code in dieselbe Struct einfügt. <code>[BurstCompile]</code> markiert: dieser Code soll von Burst übersetzt werden.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-3" />
<div class="item-body">
<span class="ln">Z. 1316</span>
<pre class="code">public void OnCreate(ref SystemState state)
{
state.RequireForUpdate&lt;PilleSimulationConfig&gt;();
}</pre>
<p class="explain"><code>OnCreate</code> läuft einmal, wenn das System erzeugt wird. <code>RequireForUpdate</code> sagt: "Führe <code>OnUpdate</code> nur aus, solange mindestens eine <code>PilleSimulationConfig</code>-Entity existiert" — verhindert Fehler, bevor der Bootstrap sie erzeugt hat.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-4" />
<div class="item-body">
<span class="ln">Z. 2122</span>
<pre class="code">var config = SystemAPI.GetSingleton&lt;PilleSimulationConfig&gt;();
float deltaTime = SystemAPI.Time.DeltaTime;</pre>
<p class="explain">Liest die aktuelle Config (Grenzen, Höhe). <code>DeltaTime</code> ist die Zeit seit dem letzten Frame (z.B. 0.016s bei 60 FPS) — macht Bewegung Framerate-unabhängig.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-5" />
<div class="item-body">
<span class="ln">Z. 2430</span>
<pre class="code">var job = new MoveAndBounceJob
{
DeltaTime = deltaTime,
BoundsMin = config.BoundsMin,
BoundsMax = config.BoundsMax,
PlayHeight = config.PlayHeight
};</pre>
<p class="explain">Baut ein "Job"-Objekt: ein Päckchen mit allen Werten, die für <em>jede</em> Pille gleich sind (Zeit, Grenzen, Höhe) — der Job wird gleich für jede Pille einmal ausgeführt.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-6" />
<div class="item-body">
<span class="ln">Z. 32</span>
<pre class="code">state.Dependency = job.ScheduleParallel(state.Dependency);</pre>
<p class="explain"><code>ScheduleParallel</code> reiht den Job in Unitys Job-System ein, das ihn über mehrere CPU-Kerne gleichzeitig abarbeitet. <code>state.Dependency</code> sorgt dafür, dass andere Systeme warten, falls sie dieselben Daten brauchen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-7" />
<div class="item-body">
<span class="ln">Z. 37</span>
<pre class="code">public partial struct MoveAndBounceJob : IJobEntity</pre>
<p class="explain"><code>IJobEntity</code> ist ein spezieller Job-Typ: die <code>Execute()</code>-Methode wird automatisch für jede Entity aufgerufen, die die dort angeforderten Komponenten besitzt — keine manuelle Schleife nötig.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-8" />
<div class="item-body">
<span class="ln">Z. 3942</span>
<pre class="code">public float DeltaTime;
public float3 BoundsMin;
public float3 BoundsMax;
public float PlayHeight;</pre>
<p class="explain">Die Kopien der Werte aus <code>OnUpdate</code> — in jedem parallelen Thread verfügbar.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-9" />
<div class="item-body">
<span class="ln">Z. 44</span>
<pre class="code">void Execute(ref LocalTransform transform, ref PilleComponent pille)</pre>
<p class="explain"><code>ref</code> bei beiden Parametern heißt: wir dürfen die Werte lesen <em>und</em> ändern. Diese Methode läuft einmal pro Pille.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-10" />
<div class="item-body">
<span class="ln">Z. 4647</span>
<pre class="code">if (math.lengthsq(pille.Velocity) < 0.0001f)
pille.Velocity = new float3(1f, 0f, 0f);</pre>
<p class="explain">Falls die Velocity (fast) null ist — z.B. gerade erst gespawnt ohne gesetzte Richtung — gib ihr eine Standardrichtung, sonst würde die Pille stehen bleiben. <code>lengthsq</code> statt <code>length</code>, weil Quadrieren schneller ist als Wurzelziehen und man zum Vergleichen keine echte Länge braucht.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-11" />
<div class="item-body">
<span class="ln">Z. 49</span>
<pre class="code">float3 pos = transform.Position + pille.Velocity * pille.Speed * DeltaTime;</pre>
<p class="explain">Die Standard-Bewegungsformel: neue Position = aktuelle Position + Richtung × Geschwindigkeit × verstrichene Zeit.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-12" />
<div class="item-body">
<span class="ln">Z. 5155</span>
<pre class="code">if (pos.x < BoundsMin.x || pos.x > BoundsMax.x)
{
pille.Velocity.x *= -1f;
pos.x = math.clamp(pos.x, BoundsMin.x, BoundsMax.x);
}</pre>
<p class="explain">Liegt die neue X-Position außerhalb der Feld-Grenzen: X-Richtung umkehren (Abprall) und Position an die Grenze "klemmen" (<code>clamp</code>), damit sie nicht durch die Wand rutscht.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-13" />
<div class="item-body">
<span class="ln">Z. 5761</span>
<pre class="code">if (pos.z < BoundsMin.z || pos.z > BoundsMax.z)
{
pille.Velocity.z *= -1f;
pos.z = math.clamp(pos.z, BoundsMin.z, BoundsMax.z);
}</pre>
<p class="explain">Dieselbe Prüfung für die Z-Achse (die "Tiefe" des Feldes).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pms-14" />
<div class="item-body">
<span class="ln">Z. 6364</span>
<pre class="code">pos.y = PlayHeight;
transform.Position = pos;</pre>
<p class="explain">Y wird fix auf <code>PlayHeight</code> gesetzt (keine Höhenbewegung), dann wird die berechnete Position zurückgeschrieben — das macht die Pille sichtbar bewegt.</p>
</div>
</li>
</ul>
</article>
<article id="f-pille-collision" class="file-card pille">
<div class="file-head">
<span class="file-tag pille">System (Burst, togglebar)</span>
<h3>PilleCollisionSystem.cs</h3>
<div class="purpose">Prüft jede Pille gegen jede andere (O(n²)) und lässt sie voneinander abprallen — bewusst simpel, nur aktiv wenn die K-Taste es einschaltet.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="pcol-1" />
<div class="item-body">
<span class="ln">Z. 16</span>
<pre class="code">using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;</pre>
<p class="explain">Neu hier: <code>Unity.Collections</code> bringt <code>NativeArray</code>/<code>Allocator</code> — manuell verwalteten Speicher außerhalb des normalen C#-Heaps (Pflicht für Burst/Jobs). <code>Unity.Jobs</code> bringt <code>IJob</code> — ein einfacher Job-Typ ohne automatischen Entity-Bezug ("führe diese eine Methode einmal aus", im Gegensatz zu <code>IJobEntity</code>).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-2" />
<div class="item-body">
<span class="ln">Z. 1013 (Kommentar)</span>
<pre class="code">// Pille-gegen-Pille Kollision. Bewusst als einfacher O(n^2) Burst-Job gebaut,
// damit man per "K"-Taste sauber A/B vergleichen kann, ...</pre>
<p class="explain">O(n²) heißt: bei doppelt so vielen Pillen gibt es viermal so viele Prüfungen. Bewusst so simpel gehalten, damit man mit K klar sehen kann, was Kollisionsprüfung an FPS kostet.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-3" />
<div class="item-body">
<span class="ln">Z. 14</span>
<pre class="code">public partial struct PilleCollisionSystem : ISystem</pre>
<p class="explain">Auffällig: <strong>kein</strong> <code>[BurstCompile]</code> auf der äußeren Struct — bewusst, weil <code>OnUpdate</code> hier "normale" Query-Aufrufe macht. Die eigentliche Rechenarbeit steckt im separaten <code>IJob</code> weiter unten, der sehr wohl <code>[BurstCompile]</code> hat.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-4" />
<div class="item-body">
<span class="ln">Z. 2325</span>
<pre class="code">var config = SystemAPI.GetSingleton&lt;PilleSimulationConfig&gt;();
if (!config.PilleCollisionEnabled)
return;</pre>
<p class="explain">Das ist die K-Taste-Abschaltung: ist Kollision nicht aktiviert, bricht das System sofort ab, ohne irgendetwas zu berechnen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-5" />
<div class="item-body">
<span class="ln">Z. 2730</span>
<pre class="code">var query = SystemAPI.QueryBuilder().WithAll&lt;PilleComponent, LocalTransform&gt;().Build();
int count = query.CalculateEntityCount();
if (count < 2)
return;</pre>
<p class="explain"><code>QueryBuilder</code> baut eine "Anfrage" an die ECS-Welt: "gib mir alle Entities mit <code>PilleComponent</code> UND <code>LocalTransform</code>". Bei weniger als 2 Pillen kann es keine Kollision geben.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-6" />
<div class="item-body">
<span class="ln">Z. 3233</span>
<pre class="code">var transforms = query.ToComponentDataArray&lt;LocalTransform&gt;(Allocator.TempJob);
var pilles = query.ToComponentDataArray&lt;PilleComponent&gt;(Allocator.TempJob);</pre>
<p class="explain">Kopiert Positions- und Bewegungsdaten <em>aller</em> Pillen in zwei einfache Arrays — nötig, weil der Job gleich direkt mit rohem Speicher arbeitet, nicht mit der Query selbst.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-7" />
<div class="item-body">
<span class="ln">Z. 35</span>
<pre class="code">float radiusSum = config.PilleRadius * 2f;</pre>
<p class="explain">Zwei Pillen "berühren" sich, wenn ihr Mittelpunkt-Abstand kleiner ist als die Summe beider Radien — bei gleich großen Pillen ist das der doppelte Radius.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-8" />
<div class="item-body">
<span class="ln">Z. 3742</span>
<pre class="code">var job = new ResolvePairCollisionsJob
{
Transforms = transforms,
Pilles = pilles,
RadiusSq = radiusSum * radiusSum
};</pre>
<p class="explain"><code>RadiusSq</code> = Radius <em>im Quadrat</em>, nicht die Wurzel — Wurzelziehen ist teuer, und zum reinen Vergleichen von Abständen braucht man sie gar nicht.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-9" />
<div class="item-body">
<span class="ln">Z. 4445</span>
<pre class="code">state.Dependency = job.Schedule(state.Dependency);
state.Dependency.Complete();</pre>
<p class="explain"><code>.Schedule(...)</code><strong>nicht</strong> <code>ScheduleParallel</code>! Der Job verändert gleichzeitig mehrere Pillen im selben Array (siehe unten) — parallel wäre das ein Datenwettlauf. <code>.Complete()</code> wartet, bis der Job wirklich fertig ist, weil wir das Ergebnis sofort brauchen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-10" />
<div class="item-body">
<span class="ln">Z. 4750</span>
<pre class="code">query.CopyFromComponentDataArray(pilles);
transforms.Dispose();
pilles.Dispose();</pre>
<p class="explain">Schreibt die (evtl. veränderten) Pillen-Daten zurück in die echten ECS-Komponenten. <code>Dispose()</code> gibt den manuell reservierten Speicher wieder frei — anders als normale C#-Objekte räumt hier kein Garbage Collector automatisch auf.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-11" />
<div class="item-body">
<span class="ln">Z. 5459</span>
<pre class="code">[BurstCompile]
public struct ResolvePairCollisionsJob : IJob
{
[ReadOnly] public NativeArray&lt;LocalTransform&gt; Transforms;
public NativeArray&lt;PilleComponent&gt; Pilles;
public float RadiusSq;</pre>
<p class="explain"><code>[ReadOnly]</code> bei <code>Transforms</code>: wir lesen Positionen nur, ändern sie nie (Sicherheits-Markierung für Burst). <code>Pilles</code> hat keine <code>[ReadOnly]</code>-Markierung, weil wir hier die Velocity bei Kollision ändern dürfen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-12" />
<div class="item-body">
<span class="ln">Z. 6366</span>
<pre class="code">int count = Transforms.Length;
for (int i = 0; i < count; i++)
{
for (int j = i + 1; j < count; j++)</pre>
<p class="explain">Zwei verschachtelte Schleifen: die äußere geht durch jede Pille <code>i</code>, die innere durch jede <em>folgende</em> Pille <code>j</code> (<code>j</code> beginnt bei <code>i+1</code>), damit jedes Paar nur einmal geprüft wird.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-13" />
<div class="item-body">
<span class="ln">Z. 6871</span>
<pre class="code">float3 delta = Transforms[i].Position - Transforms[j].Position;
float distSq = math.lengthsq(delta);
if (distSq < RadiusSq && distSq > 0.0001f)</pre>
<p class="explain">Berechnet den (quadrierten) Abstand zwischen Pille i und j. Die zweite Bedingung (<code>distSq &gt; 0.0001f</code>) verhindert eine Division durch 0, falls zwei Pillen exakt an derselben Stelle wären.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-14" />
<div class="item-body">
<span class="ln">Z. 73</span>
<pre class="code">float3 normal = math.normalize(delta);</pre>
<p class="explain">Die "Normale" — die Richtung von Pille j zu Pille i, auf Länge 1 gebracht. Das ist die Achse, an der beide Pillen abprallen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pcol-15" />
<div class="item-body">
<span class="ln">Z. 7582</span>
<pre class="code">var pi = Pilles[i];
var pj = Pilles[j];
pi.Velocity = math.reflect(pi.Velocity, normal);
pj.Velocity = math.reflect(pj.Velocity, -normal);
Pilles[i] = pi;
Pilles[j] = pj;</pre>
<p class="explain"><code>math.reflect</code> spiegelt die Geschwindigkeit an der Kollisions-Normale — physikalisch wie ein Ball, der von einer Wand abprallt, nur dass hier die "Wand" die jeweils andere Pille ist (deshalb bei <code>pj</code> die Normale umgekehrt, <code>-normal</code>). Am Ende werden beide veränderten Werte zurück ins Array geschrieben.</p>
</div>
</li>
</ul>
</article>
<article id="f-pille-bootstrap" class="file-card pille">
<div class="file-head">
<span class="file-tag pille">MonoBehaviour (Brücke)</span>
<h3>PilleDotsBootstrap.cs</h3>
<div class="purpose">Das einzige "normale" Unity-Script im Pille-System. Liest Tasten, baut die Entity-Welt auf, spawnt/löscht Pillen. Bewegung und Kollision passieren NICHT hier, sondern in den beiden Systemen oben.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="pboot-1" />
<div class="item-body">
<span class="ln">Z. 17</span>
<pre class="code">using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;
using UnityEngine.Rendering;</pre>
<p class="explain">Neu: <code>Unity.Rendering</code> bringt <code>RenderMeshUtility</code>/<code>RenderMeshDescription</code>/<code>RenderMeshArray</code>/<code>MaterialMeshInfo</code> — die Werkzeuge, um einer Entity zur Laufzeit ein sichtbares Mesh+Material zu geben (kommt aus dem Entities-Graphics-Paket). <code>UnityEngine</code> bringt die normalen Unity-Klassen (<code>MonoBehaviour</code>, <code>Input</code>, <code>Debug</code>, <code>GUI</code>). <code>UnityEngine.Rendering</code> bringt <code>ShadowCastingMode</code>.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-2" />
<div class="item-body">
<span class="ln">Z. 15</span>
<pre class="code">public class PilleDotsBootstrap : MonoBehaviour</pre>
<p class="explain"><code>class</code> statt <code>struct</code>, weil das hier ein ganz normales Unity-Component ist (kein ECS) — es lebt auf einem GameObject in der Szene, kein Burst.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-3" />
<div class="item-body">
<span class="ln">Z. 1726</span>
<pre class="code">[SerializeField] private Mesh pilleMesh;
[SerializeField] private Material pilleMaterial;
[SerializeField] private float speed = 5f;
[SerializeField] private float pilleRadius = 0.5f;
[SerializeField] private float spawnHeight = 1f;
[SerializeField] private string wallTag = "Wall";
[SerializeField] private float actionDelay = 0.05f;</pre>
<p class="explain">Alle Werte, die du im Inspector einstellst: Mesh/Material (Pflicht), Bewegungstempo, Kollisionsradius, Spawn-Höhe, welcher Tag als "Wand" zählt, und wie viel Zeit mindestens zwischen zwei Tastendruck-Aktionen liegen muss (verhindert z.B. 60 Spawns pro Sekunde bei gehaltener Leertaste).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-4" />
<div class="item-body">
<span class="ln">Z. 2837</span>
<pre class="code">private EntityManager entityManager;
private Entity pilleTemplateEntity;
private EntityQuery pilleQuery;
private EntityQuery configQuery;
private int pilleCount = 0;
private float lastActionTime = 0f;
private bool collisionEnabledCache = false;
private Texture2D backgroundTexture;</pre>
<p class="explain">Interne Zustände: der Zugang zur ECS-Welt (<code>entityManager</code>), die als Vorlage dienende Template-Entity, zwei "gespeicherte Suchanfragen" (Queries, schneller als sie jedes Mal neu zu bauen), ein Zähler fürs Overlay, der Zeitpunkt der letzten Aktion, ein gecachter Kollisions-Status fürs Overlay, und die Hintergrundtextur für die Lesbarkeit.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-5" />
<div class="item-body">
<span class="ln">Z. 4146</span>
<pre class="code">if (pilleMesh == null || pilleMaterial == null)
{
Debug.LogError("❌ ...");
enabled = false;
return;
}</pre>
<p class="explain">Frühzeitiger Abbruch, falls du vergisst, Mesh/Material im Inspector zu setzen — <code>enabled = false</code> schaltet das ganze Script ab, statt später mit unklaren Fehlern abzustürzen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-6" />
<div class="item-body">
<span class="ln">Z. 48</span>
<pre class="code">entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;</pre>
<p class="explain">Holt sich den Zugang zur "Standard-ECS-Welt", die Unity beim Spielstart automatisch anlegt. Der <code>EntityManager</code> ist das zentrale Werkzeug, um Entities/Komponenten zu erstellen, zu lesen und zu ändern.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-7" />
<div class="item-body">
<span class="ln">Z. 5058</span>
<pre class="code">SetupSimulationConfig();
SetupPilleTemplate();
pilleQuery = entityManager.CreateEntityQuery(...);
configQuery = entityManager.CreateEntityQuery(...);
backgroundTexture = MakeTex(...);
Debug.Log("✅ ...");</pre>
<p class="explain">Ruft die zwei Setup-Methoden auf (Details unten), baut die zwei Queries einmalig, erzeugt die Hintergrundtextur fürs Overlay, und bestätigt in der Console, dass alles bereit ist.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-8" />
<div class="item-body">
<span class="ln">Z. 6374 — SetupSimulationConfig()</span>
<pre class="code">var wallBounds = ComputeWallBounds();
float margin = pilleRadius + 0.1f;
var configEntity = entityManager.CreateEntity(typeof(PilleSimulationConfig));
entityManager.SetComponentData(configEntity, new PilleSimulationConfig { ... });</pre>
<p class="explain">Berechnet die Feld-Grenzen aus den Wänden, zieht einen kleinen Sicherheitsabstand (<code>margin</code>) ab, damit Pillen nicht IN der Wand spawnen/stecken, erstellt dann die eine Singleton-Entity und befüllt sie.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-9" />
<div class="item-body">
<span class="ln">Z. 77100 — ComputeWallBounds()</span>
<pre class="code">var walls = GameObject.FindGameObjectsWithTag(wallTag);
if (walls.Length == 0) { ... return new Bounds(...); }
Bounds combined = default;
foreach (var wall in walls) { ... combined.Encapsulate(rend.bounds); }</pre>
<p class="explain">Sucht alle Szenen-Objekte mit Tag "Wall", liest deren Renderer-Bounds (die räumliche Ausdehnung) und vereinigt sie zu einer Gesamt-Box (<code>Encapsulate</code>) — das ergibt automatisch die Feld-Größe, egal wie die Wände gebaut sind. Gibt es keine Wände, wird eine Standard-Größe (50×10×100) benutzt statt abzustürzen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-10" />
<div class="item-body">
<span class="ln">Z. 104107 — SetupPilleTemplate() Teil 1</span>
<pre class="code">pilleTemplateEntity = entityManager.CreateEntity(
typeof(PilleComponent),
typeof(LocalTransform),
typeof(LocalToWorld));</pre>
<p class="explain">Erstellt eine einzelne "Vorlagen-Entity" mit den drei Basis-Komponenten, die jede Pille braucht. <code>LocalToWorld</code> wird zusätzlich zu <code>LocalTransform</code> gebraucht, damit das Rendering-System weiß, wo im Raum die Entity tatsächlich steht.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-11" />
<div class="item-body">
<span class="ln">Z. 109117 — SetupPilleTemplate() Teil 2</span>
<pre class="code">var desc = new RenderMeshDescription(ShadowCastingMode.On, receiveShadows: true);
var renderMeshArray = new RenderMeshArray(new[] { pilleMaterial }, new[] { pilleMesh });
RenderMeshUtility.AddComponents(pilleTemplateEntity, entityManager, desc, renderMeshArray,
MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0));</pre>
<p class="explain">Das ist der Kern, der DOTS-Entities überhaupt sichtbar macht: <code>RenderMeshUtility.AddComponents</code> hängt der Entity alle nötigen Rendering-Komponenten an (Mesh, Material, Bounds) — ohne diesen Aufruf bleibt jede Entity unsichtbar, egal wie korrekt die Bewegung läuft (das war genau das Problem in der Baker/SubScene-Sackgasse davor).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-12" />
<div class="item-body">
<span class="ln">Z. 119126 — SetupPilleTemplate() Teil 3</span>
<pre class="code">entityManager.SetComponentData(pilleTemplateEntity, LocalTransform.FromPosition(...));
entityManager.SetComponentData(pilleTemplateEntity, new PilleComponent { Velocity = ..., Speed = speed });</pre>
<p class="explain">Setzt die Startwerte der Vorlage: Position (0, spawnHeight, 0) und eine Standard-Velocity/Speed. Jede spätere Kopie startet mit diesen Werten, bevor sie individuell überschrieben werden.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-13" />
<div class="item-body">
<span class="ln">Z. 128131 — SetupPilleTemplate() Teil 4</span>
<pre class="code">// WICHTIG: Prefab-Tag, damit dieses Template NICHT von den Queries/Systemen
// (Bewegung, Kollision, Zählung) erfasst wird ...
entityManager.AddComponent&lt;Prefab&gt;(pilleTemplateEntity);</pre>
<p class="explain">Der <code>Prefab</code>-Tag ist ein eingebauter ECS-Mechanismus: Entities mit diesem Tag werden von normalen Queries automatisch <em>ausgeschlossen</em> (die Vorlage selbst bewegt sich also nicht mit). Ruft man <code>Instantiate()</code> auf, entfernt Unity den Tag bei der Kopie automatisch — die Kopie ist dann eine ganz normale, aktive Pille.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-14" />
<div class="item-body">
<span class="ln">Z. 134160 — Update()</span>
<pre class="code">if (Input.GetKeyDown(KeyCode.K)) ToggleCollision();
if (Time.time - lastActionTime < actionDelay) return;
if (Input.GetKey(KeyCode.Space)) { SpawnPille(); ... }
else if (Input.GetKey(KeyCode.M)) { DeletePille(); ... }
else if (Input.GetKey(KeyCode.C)) { for (...) SpawnPille(); ... }</pre>
<p class="explain">Läuft jeden Frame. <code>K</code> wird mit <code>GetKeyDown</code> geprüft (reagiert nur auf den Moment des Drückens) und ist von der Cooldown-Sperre ausgenommen. Die anderen drei Tasten nutzen <code>GetKey</code> (reagieren, solange gedrückt gehalten wird) und respektieren <code>actionDelay</code> als Mindestabstand zwischen zwei Aktionen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-15" />
<div class="item-body">
<span class="ln">Z. 162173 — SpawnPille() Teil 1</span>
<pre class="code">float3 spawnPos = new float3(0, spawnHeight, 0);
int existing = pilleQuery.CalculateEntityCount();
if (existing > 0)
{
var transforms = pilleQuery.ToComponentDataArray&lt;LocalTransform&gt;(Allocator.Temp);
int idx = UnityEngine.Random.Range(0, transforms.Length);
spawnPos = transforms[idx].Position;
transforms.Dispose();
}</pre>
<p class="explain">Bestimmt den Spawn-Ort: Standardmäßig die Mitte, aber falls schon Pillen existieren, wird eine zufällige davon ausgewählt und die neue Pille dort platziert — deshalb "wachsen" die Pillen-Haufen organisch statt alle exakt an einem Punkt zu spawnen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-16" />
<div class="item-body">
<span class="ln">Z. 175187 — SpawnPille() Teil 2</span>
<pre class="code">Entity newPille = entityManager.Instantiate(pilleTemplateEntity);
float angle = UnityEngine.Random.Range(0f, math.PI * 2f);
var velocity = new float3(math.cos(angle), 0, math.sin(angle));
entityManager.SetComponentData(newPille, LocalTransform.FromPosition(spawnPos));
entityManager.SetComponentData(newPille, new PilleComponent { Velocity = velocity, Speed = speed });
pilleCount++;</pre>
<p class="explain"><code>Instantiate</code> erzeugt eine echte Kopie der Vorlage (inkl. Mesh/Material, ohne den Prefab-Tag). Ein zufälliger Winkel (0 bis 360°, in Radiant) ergibt eine zufällige horizontale Startrichtung via Kosinus/Sinus. Dann werden Position und Bewegungsrichtung individuell gesetzt.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-17" />
<div class="item-body">
<span class="ln">Z. 190199 — DeletePille()</span>
<pre class="code">if (pilleQuery.CalculateEntityCount() == 0) return;
var entities = pilleQuery.ToEntityArray(Allocator.Temp);
entityManager.DestroyEntity(entities[0]);
entities.Dispose();
pilleCount--;</pre>
<p class="explain">Holt sich alle existierenden Pillen-Entities als Liste und löscht einfach die erste — welche das konkret ist, ist beliebig, es geht nur darum, den Bestand zu verringern.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-18" />
<div class="item-body">
<span class="ln">Z. 201211 — ToggleCollision()</span>
<pre class="code">var configEntity = configQuery.GetSingletonEntity();
var cfg = entityManager.GetComponentData&lt;PilleSimulationConfig&gt;(configEntity);
cfg.PilleCollisionEnabled = !cfg.PilleCollisionEnabled;
entityManager.SetComponentData(configEntity, cfg);
collisionEnabledCache = cfg.PilleCollisionEnabled;</pre>
<p class="explain">Structs in C# sind Werttypen — man kann eine Komponente nicht "an Ort und Stelle" ändern, man muss sie <em>lesen</em>, in der Kopie den Wert umdrehen (<code>!cfg...</code>) und dann <em>komplett zurückschreiben</em>. <code>collisionEnabledCache</code> ist eine Kopie nur fürs Overlay, damit <code>OnGUI</code> nicht jeden Frame extra die ECS-Welt abfragen muss.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-19" />
<div class="item-body">
<span class="ln">Z. 213219 — MakeTex()</span>
<pre class="code">var tex = new Texture2D(1, 1);
tex.SetPixel(0, 0, color);
tex.Apply();
return tex;</pre>
<p class="explain">Erzeugt zur Laufzeit eine winzige 1×1-Pixel-Textur in einer Farbe — beim Zeichnen wird sie auf ein großes Rechteck gestreckt, was optisch wie eine einfarbige Fläche aussieht. Günstiger, als eine Bild-Datei mitzuliefern.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-20" />
<div class="item-body">
<span class="ln">Z. 221230 — OnGUI()</span>
<pre class="code">GUI.DrawTexture(new Rect(5, 5, 430, 90), backgroundTexture);
GUI.color = Color.white;
GUI.Label(..., $"Pillen: {pilleCount} | FPS: {(int)(1f / Time.deltaTime)}");</pre>
<p class="explain">Zeichnet erst die dunkle Hintergrundbox (für Lesbarkeit/Kontrast), dann die Text-Labels in Weiß darüber. Die FPS-Berechnung <code>1f / Time.deltaTime</code> ist der Kehrwert der Frame-Zeit — bei 0.016s Frame-Zeit ergibt das ≈62 FPS.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="pboot-21" />
<div class="item-body">
<span class="ln">Z. 232236 — OnDestroy()</span>
<pre class="code">if (backgroundTexture != null)
Destroy(backgroundTexture);</pre>
<p class="explain">Räumt die manuell erzeugte Textur beim Beenden/Entfernen des Objekts auf — Texturen werden vom Garbage Collector nicht automatisch vollständig freigegeben (sie belegen auch GPU-Speicher).</p>
</div>
</li>
</ul>
</article>
<!-- ====================== SYSTEM: DIRECT STRIKE ====================== -->
<section class="system-header" id="ds-section">
<h2>System 2 — Skirmish (Kampf-Kern)</h2>
<p>Phase 1+2 aus dem Implementierungsplan: zwei Teams, Lane-Bewegung, Grid-basiertes Targeting, Kampf, Tod. 9 Dateien. Noch OHNE Base/Placement-UI, Gore/Physik und Upgrades (Phase 35).</p>
</section>
<article id="f-sk-unitdef" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">ScriptableObject (Editor-Daten)</span>
<h3>Skirmish/Data/UnitDefinition.cs</h3>
<div class="purpose">Kein ECS-Code — eine Vorlage, die du im Editor als Asset anlegst und mit Werten befüllst (z.B. "Grunt", "Archer"). Wird beim Spawnen in ECS-Komponenten übersetzt.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="skud-1" />
<div class="item-body">
<span class="ln">Z. 314</span>
<pre class="code">public enum AttackType : byte { Melee = 0, Ranged = 1 }
public enum TargetType : byte { Ground = 0 }</pre>
<p class="explain">Zwei Aufzählungstypen. <code>: byte</code> heißt, sie werden intern als 1-Byte-Zahl gespeichert statt der C#-Standardgröße (4 Byte, int) — spart Speicher, wenn später tausende Units diese Werte tragen. <code>TargetType</code> hat aktuell nur "Ground", ist aber schon vorbereitet für z.B. "Air" später.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skud-2" />
<div class="item-body">
<span class="ln">Z. 17</span>
<pre class="code">[CreateAssetMenu(fileName = "UnitDefinition", menuName = "Skirmish/Unit Definition")]</pre>
<p class="explain">Dieses Attribut erzeugt den Rechtsklick-Menüpunkt "Create → Skirmish → Unit Definition" im Project-Fenster — so legst du neue Unit-Typen als Assets an, ohne Code zu schreiben.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skud-3" />
<div class="item-body">
<span class="ln">Z. 18</span>
<pre class="code">public class UnitDefinition : ScriptableObject</pre>
<p class="explain"><code>ScriptableObject</code> statt <code>MonoBehaviour</code>: lebt nicht auf einem GameObject in der Szene, sondern als eigenständige Asset-Datei im Project-Fenster — genau richtig für "Daten, die mehrfach wiederverwendet werden" (viele Units können dieselbe Definition referenzieren).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skud-4" />
<div class="item-body">
<span class="ln">Z. 2021</span>
<pre class="code">public string unitId = "Unit";
public int cost = 10;</pre>
<p class="explain">Ein Name zur Wiedererkennung und die Gold-Kosten — <code>cost</code> wird erst ab Phase 3 (Base/Shop) tatsächlich benutzt, ist aber jetzt schon Teil der Definition.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skud-5" />
<div class="item-body">
<span class="ln">Z. 2430 (Header "Combat")</span>
<pre class="code">public float maxHealth = 100f;
public float moveSpeed = 3f;
public float attackDamage = 10f;
public float attackRange = 1.5f;
public float attackCooldown = 1f;
public AttackType attackType = AttackType.Melee;
public TargetType targetType = TargetType.Ground;</pre>
<p class="explain">Die eigentlichen Kampfwerte, die du im Inspector pro Unit-Typ einstellst: maximales Leben, Lauftempo, Schaden pro Treffer, wie nah ein Ziel sein muss, und wie viele Sekunden zwischen zwei Angriffen liegen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skud-6" />
<div class="item-body">
<span class="ln">Z. 3234 (Header "Rendering")</span>
<pre class="code">public Mesh mesh;
public Material material;</pre>
<p class="explain">Wie sieht die Unit aus — genau wie <code>pilleMesh</code>/<code>pilleMaterial</code> beim Pille-Bootstrap, nur diesmal pro Unit-Typ statt global.</p>
</div>
</li>
</ul>
</article>
<article id="f-sk-unitcomp" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">Komponenten (Laufzeit-Daten)</span>
<h3>Skirmish/Components/UnitComponents.cs</h3>
<div class="purpose">Die ECS-Komponenten, die eine gespawnte Kampf-Unit tatsächlich trägt — abgeleitet aus UnitDefinition, aber davon losgelöst (wichtig für Upgrades später).</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="skuc-1" />
<div class="item-body">
<span class="ln">Team-Struct</span>
<pre class="code">public struct Team : IComponentData
{
public byte Value;
}</pre>
<p class="explain">0 = Team A, 1 = Team B. Ein <code>byte</code> reicht locker für zwei Teams (in Zukunft theoretisch bis zu 256).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skuc-2" />
<div class="item-body">
<span class="ln">CombatStats-Struct</span>
<pre class="code">public struct CombatStats : IComponentData
{
public float MoveSpeed;
public float AttackDamage;
public float AttackRange;
public float AttackCooldown;
public byte AttackType;
}</pre>
<p class="explain">Das ist der zentrale Trick aus dem Plan: Systeme lesen <strong>nur diese Komponente</strong>, nie <code>UnitDefinition</code> direkt. Der Wert wird einmal beim Spawnen aus der Definition (später: × Upgrade-Multiplikator) hineinkopiert. So kann man später Upgrades einbauen, ohne dass irgendein Kampf-System umgeschrieben werden muss.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skuc-3" />
<div class="item-body">
<span class="ln">Health-Struct</span>
<pre class="code">public struct Health : IComponentData
{
public float Current;
public float Max;
}</pre>
<p class="explain">Aktuelles und maximales Leben getrennt gespeichert — <code>Max</code> wird für Phase 3+ gebraucht (z.B. eine Lebensbalken-Anzeige "Current / Max").</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skuc-4" />
<div class="item-body">
<span class="ln">AttackCooldownTimer-Struct</span>
<pre class="code">public struct AttackCooldownTimer : IComponentData
{
public float Value;
}</pre>
<p class="explain">Zählt in Sekunden runter, bis die Unit wieder angreifen darf — analog zu einer Abklingzeit bei Fähigkeiten.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skuc-5" />
<div class="item-body">
<span class="ln">Target-Struct</span>
<pre class="code">public struct Target : IComponentData
{
public Entity Value;
}</pre>
<p class="explain">Speichert, WELCHE andere Entity gerade als Kampf-Ziel gilt. <code>Entity.Null</code> heißt "kein Ziel gerade" — dann läuft die Unit stattdessen weiter Richtung Gegner-Base.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skuc-6" />
<div class="item-body">
<span class="ln">UnitTypeId-Struct</span>
<pre class="code">public struct UnitTypeId : IComponentData
{
public int Value;
}</pre>
<p class="explain">Merkt sich, aus welchem Eintrag im <code>unitTypes</code>-Array diese Unit stammt — noch ungenutzt, wird in Phase 4 gebraucht, um beim Tod die richtigen Gore-Teile (Kopf/Arm/Waffe) zuzuordnen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skuc-7" />
<div class="item-body">
<span class="ln">DeadTag-Struct</span>
<pre class="code">public struct DeadTag : IComponentData
{
}</pre>
<p class="explain">Ein "Tag" ohne jegliche Daten (leerer Struct-Body) — dient nur als Markierung "diese Entity ist tot, wird gleich entfernt". Systeme können mit <code>WithNone&lt;DeadTag&gt;</code> tote Units einfach aus ihren Abfragen ausschließen.</p>
</div>
</li>
</ul>
</article>
<article id="f-sk-movecomp" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">Komponente</span>
<h3>Skirmish/Components/MovementComponents.cs</h3>
<div class="purpose">Ein einziges Feld: wohin läuft die Unit, wenn sie kein Kampfziel hat.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="skmc-1" />
<div class="item-body">
<span class="ln">Gesamte Datei</span>
<pre class="code">public struct LaneMoveTarget : IComponentData
{
public float3 Value;
}</pre>
<p class="explain">Ein fester Zielpunkt — die gegnerische Base. Team A bekommt beim Spawn die Position von Team B eingetragen und umgekehrt. Der Kommentar in der Datei merkt schon an: für eine später geknickte Lane (mehrere Wegpunkte) würde man hier ein <code>BlobArray&lt;float3&gt;</code> + einen Index ergänzen, ohne <code>LaneMovementSystem</code> umbauen zu müssen.</p>
</div>
</li>
</ul>
</article>
<article id="f-sk-lanemove" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">System (Burst, parallel)</span>
<h3>Skirmish/Systems/LaneMovementSystem.cs</h3>
<div class="purpose">Läuft geradeaus Richtung Gegner-Base. Sobald ein Kampfziel gesetzt ist, bleibt die Unit stehen — CombatSystem übernimmt.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="sklm-1" />
<div class="item-body">
<span class="ln">OnCreate</span>
<pre class="code">public void OnCreate(ref SystemState state)
{
state.RequireForUpdate&lt;CombatStats&gt;();
}</pre>
<p class="explain">Wie beim Pille-Movement-System: läuft erst, sobald mindestens eine Unit mit Kampfwerten existiert.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sklm-2" />
<div class="item-body">
<span class="ln">OnUpdate</span>
<pre class="code">var job = new MoveTowardTargetJob { DeltaTime = SystemAPI.Time.DeltaTime };
state.Dependency = job.ScheduleParallel(state.Dependency);</pre>
<p class="explain">Baut den Job mit der aktuellen Frame-Zeit und schickt ihn parallel über alle Units — exakt dasselbe Muster wie <code>PilleMovementSystem</code>.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sklm-3" />
<div class="item-body">
<span class="ln">[WithNone(typeof(DeadTag))]</span>
<pre class="code">[BurstCompile]
[WithNone(typeof(DeadTag))]
public partial struct MoveTowardTargetJob : IJobEntity</pre>
<p class="explain">Dieses Attribut auf dem Job selbst filtert automatisch tote Units aus der Abfrage heraus — Leichen sollen sich schließlich nicht mehr bewegen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sklm-4" />
<div class="item-body">
<span class="ln">Execute-Signatur</span>
<pre class="code">void Execute(ref LocalTransform transform, in CombatStats stats, in LaneMoveTarget laneTarget, in Target target)</pre>
<p class="explain"><code>ref</code> bei <code>transform</code> (wir ändern die Position), <code>in</code> bei den anderen drei (wir lesen sie nur, ändern sie hier nicht — schneller als <code>ref</code>, weil Burst dann weiß, dass es sich keine Rückschreibe-Gedanken machen muss).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sklm-5" />
<div class="item-body">
<span class="ln">Ziel-Check</span>
<pre class="code">if (target.Value != Entity.Null)
return;</pre>
<p class="explain">Hat die Unit gerade ein Kampfziel, wird hier sofort abgebrochen — <code>CombatSystem</code> kümmert sich dann ums Stehenbleiben-und-Angreifen, <code>LaneMovementSystem</code> lässt die Finger davon.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sklm-6" />
<div class="item-body">
<span class="ln">Restweg-Berechnung</span>
<pre class="code">float3 toTarget = laneTarget.Value - transform.Position;
float dist = math.length(toTarget);
if (dist < 0.05f)
return;</pre>
<p class="explain">Vektor und Distanz zum Ziel. Ist die Unit praktisch schon da (unter 5 cm entfernt), wird nichts mehr bewegt — verhindert ein sichtbares "Zittern" durch minimale Überschreitungen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sklm-7" />
<div class="item-body">
<span class="ln">Bewegung mit Schritt-Begrenzung</span>
<pre class="code">float3 dir = toTarget / dist;
transform.Position += dir * math.min(stats.MoveSpeed * DeltaTime, dist);</pre>
<p class="explain">Normiert die Richtung (Division durch die eigene Länge = Länge 1), bewegt sich dann Richtung Ziel — <code>math.min(...)</code> sorgt dafür, dass die Unit niemals über das Ziel <em>hinausschießt</em>, auch nicht bei großen Zeitsprüngen (z.B. kurzem Frame-Ruckler).</p>
</div>
</li>
</ul>
</article>
<article id="f-sk-grid" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">System (Burst, Singleton-Container)</span>
<h3>Skirmish/Systems/BuildLaneGridSystem.cs</h3>
<div class="purpose">Baut jeden Frame ein "Bucket-Grid" entlang der Lane, damit TargetingSystem nicht jede Unit gegen jede prüfen muss (das wäre bei 5.000 Units zu langsam).</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="skgrid-1" />
<div class="item-body">
<span class="ln">LaneGridSingleton-Struct</span>
<pre class="code">public struct LaneGridSingleton : IComponentData
{
public NativeParallelMultiHashMap&lt;int, Entity&gt; Buckets;
public float BucketSize;
}</pre>
<p class="explain">Eine "HashMap", die zu einer Zahl (dem Bucket-Index) mehrere Entities speichern kann (deshalb "Multi"). Man kann eine NativeContainer wie diese direkt in einer Komponente speichern, solange sie unmanaged bleibt — hier als Singleton, damit jedes System per <code>SystemAPI.GetSingleton</code> Zugriff bekommt.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skgrid-2" />
<div class="item-body">
<span class="ln">[UpdateBefore(typeof(TargetingSystem))]</span>
<pre class="code">[UpdateBefore(typeof(TargetingSystem))]
public partial struct BuildLaneGridSystem : ISystem</pre>
<p class="explain">Erzwingt die Reihenfolge: dieses System muss VOR <code>TargetingSystem</code> laufen, sonst würde Targeting mit einem leeren oder veralteten Grid arbeiten.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skgrid-3" />
<div class="item-body">
<span class="ln">OnCreate</span>
<pre class="code">if (!SystemAPI.HasSingleton&lt;LaneGridSingleton&gt;())
{
var entity = state.EntityManager.CreateEntity(typeof(LaneGridSingleton));
state.EntityManager.SetComponentData(entity, new LaneGridSingleton
{
Buckets = new NativeParallelMultiHashMap&lt;int, Entity&gt;(65536, Allocator.Persistent),
BucketSize = 5f
});
}</pre>
<p class="explain">Erzeugt die Singleton-Entity nur, falls sie noch nicht existiert. Kapazität fest auf 65.536 gesetzt (statt dynamisch wachsend) — das vermeidet, dass ein Reallokieren <em>während</em> ein paralleler Job gerade hineinschreibt, Probleme macht. <code>Allocator.Persistent</code> heißt: bleibt bestehen, bis wir es explizit wieder freigeben (nicht wie <code>Temp</code>/<code>TempJob</code>, die nur kurzlebig sind).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skgrid-4" />
<div class="item-body">
<span class="ln">OnDestroy</span>
<pre class="code">if (SystemAPI.HasSingleton&lt;LaneGridSingleton&gt;())
{
var grid = SystemAPI.GetSingleton&lt;LaneGridSingleton&gt;();
if (grid.Buckets.IsCreated)
grid.Buckets.Dispose();
}</pre>
<p class="explain">Gibt den mit <code>Persistent</code> reservierten Speicher beim Beenden frei — das ist Pflicht, sonst merkt sich Unity im Editor ein "Speicherleck" und beschwert sich beim nächsten Play.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skgrid-5" />
<div class="item-body">
<span class="ln">OnUpdate</span>
<pre class="code">var gridRW = SystemAPI.GetSingletonRW&lt;LaneGridSingleton&gt;();
gridRW.ValueRW.Buckets.Clear();
var job = new FillGridJob { BucketSize = ..., Writer = gridRW.ValueRW.Buckets.AsParallelWriter() };
state.Dependency = job.ScheduleParallel(state.Dependency);</pre>
<p class="explain"><code>GetSingletonRW</code> gibt eine beschreibbare Referenz (statt einer Kopie). <code>Clear()</code> leert das Grid jeden Frame komplett (die alte Speicherreservierung bleibt aber erhalten — nur der Inhalt wird gelöscht). <code>AsParallelWriter()</code> erlaubt mehreren Threads gleichzeitig sicher hineinzuschreiben.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skgrid-6" />
<div class="item-body">
<span class="ln">FillGridJob-Attribute</span>
<pre class="code">[BurstCompile]
[WithAll(typeof(Team))]
[WithNone(typeof(DeadTag))]
public partial struct FillGridJob : IJobEntity</pre>
<p class="explain"><code>[WithAll(typeof(Team))]</code> ist wichtig: ohne dieses Attribut würde die automatische Query nur nach <code>LocalTransform</code> filtern (dem einzigen Parameter, den <code>Execute</code> direkt nutzt) — das würde <em>auch die Pillen</em> ins Grid mitziehen, falls beide Systeme gleichzeitig in derselben Szene laufen! <code>Team</code> ist eine Komponente, die nur Skirmish-Units haben.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skgrid-7" />
<div class="item-body">
<span class="ln">Execute</span>
<pre class="code">void Execute(Entity entity, in LocalTransform transform)
{
int bucket = (int)math.floor(transform.Position.z / BucketSize);
Writer.Add(bucket, entity);
}</pre>
<p class="explain">Berechnet, in welchen "Slot" entlang der Z-Achse (der Lane-Richtung) diese Unit fällt — z.B. bei <code>BucketSize = 5</code> landen alle Units zwischen Z=10 und Z=15 im Bucket 2. Dann wird die Entity in genau diesen Slot eingetragen. Das ist der ganze Trick: statt 5.000 gegen 5.000 zu prüfen, muss man später nur noch den eigenen Bucket + die zwei Nachbar-Buckets durchsuchen.</p>
</div>
</li>
</ul>
</article>
<article id="f-sk-targeting" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">System (Burst, parallel)</span>
<h3>Skirmish/Systems/TargetingSystem.cs</h3>
<div class="purpose">Sucht für jede Unit ein gültiges Kampfziel — aber nur, wenn nötig, nicht jeden Frame neu.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="sktgt-1" />
<div class="item-body">
<span class="ln">OnUpdate</span>
<pre class="code">var grid = SystemAPI.GetSingleton&lt;LaneGridSingleton&gt;();
var job = new AcquireTargetJob { Buckets = grid.Buckets, ..., TeamLookup = SystemAPI.GetComponentLookup&lt;Team&gt;(true), ... };
state.Dependency = job.ScheduleParallel(state.Dependency);</pre>
<p class="explain">Holt das fertige Grid (von <code>BuildLaneGridSystem</code>) und drei <code>ComponentLookup</code>s — das sind "wahlfreie Zugriffe": während der Job über EINE Unit iteriert, kann er trotzdem gezielt Daten von JEDER ANDEREN Entity nachschlagen (Team, Position, Tot-Status). Das <code>true</code> markiert die Lookups als nur-lesend.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sktgt-2" />
<div class="item-body">
<span class="ln">Execute-Signatur</span>
<pre class="code">void Execute(Entity entity, ref Target target, in Team team, in CombatStats stats, in LocalTransform transform)</pre>
<p class="explain">Läuft für jede lebende Unit (Tote sind per <code>[WithNone(typeof(DeadTag))]</code> ausgeschlossen). <code>entity</code> ist die eigene Entity — gebraucht, um sich selbst später nicht versehentlich als eigenes Ziel zu wählen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sktgt-3" />
<div class="item-body">
<span class="ln">Gültigkeits-Check des aktuellen Ziels</span>
<pre class="code">float rangeSq = stats.AttackRange * stats.AttackRange;
if (target.Value != Entity.Null)
{
bool stillValid = !DeadLookup.HasComponent(target.Value)
&& TransformLookup.HasComponent(target.Value)
&& math.distancesq(transform.Position, TransformLookup[target.Value].Position) <= rangeSq;
if (stillValid) return;
target.Value = Entity.Null;
}</pre>
<p class="explain">Bevor überhaupt neu gesucht wird: ist das aktuelle Ziel noch am Leben, existiert es noch, und ist es noch in Reichweite? Wenn ja, sofort zurück — das spart die teure Grid-Suche in den meisten Frames, weil eine Unit meist mehrere Angriffe hintereinander auf dasselbe Ziel macht.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sktgt-4" />
<div class="item-body">
<span class="ln">Bucket-Bestimmung</span>
<pre class="code">int myBucket = (int)math.floor(transform.Position.z / BucketSize);
float bestDistSq = float.MaxValue;
Entity best = Entity.Null;</pre>
<p class="explain">Der eigene Bucket (genau wie in <code>FillGridJob</code> berechnet), plus zwei Variablen, um während der Suche den bisher besten (nächsten) Kandidaten zu merken.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sktgt-5" />
<div class="item-body">
<span class="ln">Nachbar-Buckets durchsuchen</span>
<pre class="code">for (int offset = -1; offset <= 1; offset++)
{
int bucket = myBucket + offset;
if (!Buckets.TryGetFirstValue(bucket, out Entity candidate, out var it))
continue;
do { ... } while (Buckets.TryGetNextValue(out candidate, ref it));
}</pre>
<p class="explain">Durchsucht den eigenen Bucket UND je einen links/rechts davon (<code>offset -1, 0, 1</code>) — deckt so auch Fälle ab, wo ein Gegner knapp jenseits der Bucket-Grenze steht. <code>TryGetFirstValue</code>/<code>TryGetNextValue</code> ist das Standard-Muster, um alle Einträge zu einem Schlüssel aus einer <code>NativeParallelMultiHashMap</code> herauszuholen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sktgt-6" />
<div class="item-body">
<span class="ln">Kandidaten-Filter</span>
<pre class="code">if (candidate == entity) continue;
if (!TeamLookup.HasComponent(candidate) || TeamLookup[candidate].Value == team.Value) continue;
if (!TransformLookup.HasComponent(candidate)) continue;</pre>
<p class="explain">Drei Ausschlussgründe: der Kandidat ist man selbst, der Kandidat gehört zum eigenen Team (keine Freundlich-Feuer!), oder er hat gar keine Position (theoretisch nie der Fall, aber sicherheitshalber geprüft).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="sktgt-7" />
<div class="item-body">
<span class="ln">Nächstes-Ziel-Auswahl</span>
<pre class="code">float distSq = math.distancesq(transform.Position, TransformLookup[candidate].Position);
if (distSq <= rangeSq && distSq < bestDistSq)
{
bestDistSq = distSq;
best = candidate;
}</pre>
<p class="explain">Von allen gültigen Kandidaten in Reichweite wird der <em>nächste</em> gewählt — nicht irgendeiner. Nach der Schleife (Zeile ganz unten: <code>target.Value = best;</code>) wird das Ergebnis gespeichert, egal ob ein Ziel gefunden wurde oder <code>Entity.Null</code> übrig blieb.</p>
</div>
</li>
</ul>
</article>
<article id="f-sk-combat" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">System (Burst, NICHT parallel)</span>
<h3>Skirmish/Systems/CombatSystem.cs</h3>
<div class="purpose">Zieht Cooldowns runter, teilt Schaden aus, markiert Tote. Bewusst single-threaded, um Race Conditions auf geteiltes Leben zu vermeiden.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="skcb-1" />
<div class="item-body">
<span class="ln">Kommentar + [UpdateAfter]</span>
<pre class="code">// Bewusst NICHT parallelisiert (kein ScheduleParallel): zwei Units koennten
// sonst im selben Frame gleichzeitig dasselbe Ziel treffen (Race Condition ...)
[UpdateAfter(typeof(TargetingSystem))]</pre>
<p class="explain">Läuft garantiert erst, NACHDEM alle Ziele für diesen Frame feststehen. Der Grund für Single-Thread: zwei Units könnten sonst gleichzeitig (auf verschiedenen CPU-Kernen) versuchen, dieselbe fremde <code>Health</code> zu verändern — das wäre ein klassischer Daten-Wettlauf mit unvorhersehbarem Ergebnis.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skcb-2" />
<div class="item-body">
<span class="ln">ECB-Setup</span>
<pre class="code">var ecb = SystemAPI.GetSingleton&lt;EndSimulationEntityCommandBufferSystem.Singleton&gt;()
.CreateCommandBuffer(state.WorldUnmanaged);
var healthLookup = SystemAPI.GetComponentLookup&lt;Health&gt;();</pre>
<p class="explain">Ein <code>EntityCommandBuffer</code> (ECB) sammelt "Änderungswünsche" (hier: Komponente hinzufügen), die erst später sicher ausgeführt werden — man darf nämlich nicht mitten in einer laufenden Abfrage direkt Komponenten hinzufügen/entfernen. <code>healthLookup</code> hier <em>ohne</em> <code>true</code> — also lesend UND schreibend, weil wir gleich fremdes Leben verändern.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skcb-3" />
<div class="item-body">
<span class="ln">Die Haupt-Schleife</span>
<pre class="code">foreach (var (target, stats, cooldown) in
SystemAPI.Query&lt;RefRO&lt;Target&gt;, RefRO&lt;CombatStats&gt;, RefRW&lt;AttackCooldownTimer&gt;&gt;()
.WithNone&lt;DeadTag&gt;())</pre>
<p class="explain">Ein <code>SystemAPI.Query</code>-<code>foreach</code> ist die "von Hand geschriebene", aber trotzdem Burst-schnelle Alternative zu einem separaten <code>IJobEntity</code> — hier bewusst gewählt, weil wir single-threaded bleiben wollen. <code>RefRO</code> = nur lesen, <code>RefRW</code> = lesen und schreiben.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skcb-4" />
<div class="item-body">
<span class="ln">Cooldown runterzählen</span>
<pre class="code">cooldown.ValueRW.Value -= deltaTime;
if (target.ValueRO.Value == Entity.Null) continue;
if (cooldown.ValueRO.Value > 0f) continue;</pre>
<p class="explain">Jede Unit zählt ihren Cooldown IMMER runter (auch ohne Ziel). Dann zwei Abbruchgründe: kein Ziel gesetzt, oder Cooldown noch nicht abgelaufen — <code>continue</code> springt zur nächsten Unit in der Schleife.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skcb-5" />
<div class="item-body">
<span class="ln">Schaden anwenden</span>
<pre class="code">if (!healthLookup.HasComponent(target.ValueRO.Value)) continue;
var targetHealth = healthLookup[target.ValueRO.Value];
targetHealth.Current -= stats.ValueRO.AttackDamage;
healthLookup[target.ValueRO.Value] = targetHealth;
cooldown.ValueRW.Value = stats.ValueRO.AttackCooldown;</pre>
<p class="explain">Liest das Leben des Ziels über den Lookup (nicht über die eigene Query — das Ziel ist ja eine ANDERE Entity), zieht den Schaden ab, schreibt zurück, und setzt den eigenen Cooldown auf den vollen Wert zurück (nächster Angriff erst wieder nach <code>AttackCooldown</code> Sekunden).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skcb-6" />
<div class="item-body">
<span class="ln">Tod markieren</span>
<pre class="code">if (targetHealth.Current <= 0f)
{
ecb.AddComponent&lt;DeadTag&gt;(target.ValueRO.Value);
}</pre>
<p class="explain">Fällt das Leben auf 0 oder darunter, wird dem <em>Ziel</em> (nicht dem Angreifer!) über den ECB der <code>DeadTag</code> angehängt. Die Entity wird dadurch NICHT sofort gelöscht — das übernimmt erst <code>DeathSystem</code> im nächsten Durchlauf.</p>
</div>
</li>
</ul>
</article>
<article id="f-sk-death" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">System (einfach)</span>
<h3>Skirmish/Systems/DeathSystem.cs</h3>
<div class="purpose">Räumt tote Units weg. In Phase 4 kommt hier das Gore-Spawning dazu (braucht Unity.Physics, noch nicht installiert).</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="skdth-1" />
<div class="item-body">
<span class="ln">Struct-Deklaration</span>
<pre class="code">public partial struct DeathSystem : ISystem</pre>
<p class="explain">Kein <code>[BurstCompile]</code> hier — bewusst, weil <code>Debug.Log</code> (in der Schleife) keine Burst-kompatible Operation ist. Da hier ohnehin nur wenige Units pro Frame sterben, kostet das kaum Performance.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skdth-2" />
<div class="item-body">
<span class="ln">OnCreate</span>
<pre class="code">state.RequireForUpdate&lt;DeadTag&gt;();</pre>
<p class="explain">Läuft nur, solange mindestens eine Entity gerade den <code>DeadTag</code> trägt — in den meisten Frames (niemand stirbt gerade) macht dieses System dadurch komplett gar nichts.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skdth-3" />
<div class="item-body">
<span class="ln">OnUpdate</span>
<pre class="code">var ecb = SystemAPI.GetSingleton&lt;EndSimulationEntityCommandBufferSystem.Singleton&gt;()
.CreateCommandBuffer(state.WorldUnmanaged);
foreach (var (deadTag, entity) in SystemAPI.Query&lt;RefRO&lt;DeadTag&gt;&gt;().WithEntityAccess())
{
ecb.DestroyEntity(entity);
}</pre>
<p class="explain">Findet alle als tot markierten Entities und reiht sie zum Löschen ein (wieder über einen ECB statt direkt, aus demselben Grund wie in <code>CombatSystem</code>). <code>WithEntityAccess()</code> gibt zusätzlich zur Komponente auch die Entity-ID selbst mit in die Schleife.</p>
</div>
</li>
</ul>
</article>
<article id="f-sk-bootstrap" class="file-card sk">
<div class="file-head">
<span class="file-tag sk">MonoBehaviour (Brücke)</span>
<h3>Skirmish/SkirmishBootstrap.cs</h3>
<div class="purpose">Pendant zu PilleDotsBootstrap: baut pro Unit-Typ × Team ein Template, liest Debug-Tasten (1/2/3), zeigt das Overlay.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="skboot-1" />
<div class="item-body">
<span class="ln">Felder (Header "Unit-Typen"/"Lane"/"Debug")</span>
<pre class="code">[SerializeField] private UnitDefinition[] unitTypes;
[SerializeField] private string wallTag = "Wall";
[SerializeField] private float spawnHeight = 1f;
[SerializeField] private float spawnSpreadX = 8f;
[SerializeField] private int debugWaveCountPerTeam = 20;</pre>
<p class="explain">Das Array, das du im Inspector mit deinen UnitDefinition-Assets befüllst. <code>spawnSpreadX</code> ist eine seitliche Streuung beim Spawnen, damit nicht alle Units exakt übereinander starten.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-2" />
<div class="item-body">
<span class="ln">private Felder</span>
<pre class="code">private Entity[] templatesTeam0;
private Entity[] templatesTeam1;
private float3 team0BasePos;
private float3 team1BasePos;
private EntityQuery aliveQuery;</pre>
<p class="explain">Zwei Template-Arrays (ein Template pro Unit-Typ, getrennt nach Team, weil jedes Team ein anderes <code>LaneMoveTarget</code> braucht), die zwei Basis-Positionen an den Lane-Enden, und eine Query, die fürs Overlay zählt, wie viele Units gerade leben.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-3" />
<div class="item-body">
<span class="ln">Start() — Absicherung</span>
<pre class="code">if (unitTypes == null || unitTypes.Length == 0)
{
Debug.LogError("❌ ...");
enabled = false;
return;
}</pre>
<p class="explain">Bricht sauber ab, falls im Inspector kein einziger Unit-Typ eingetragen wurde.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-4" />
<div class="item-body">
<span class="ln">Start() — Lane-Basen</span>
<pre class="code">var laneBounds = ComputeLaneBounds();
team0BasePos = new float3(0, spawnHeight, laneBounds.min.z + 2f);
team1BasePos = new float3(0, spawnHeight, laneBounds.max.z - 2f);</pre>
<p class="explain">Nutzt dieselbe Wand-basierte Grenzenberechnung wie beim Pille-System (nur diesmal als eigene Methode), aber interpretiert die Z-Achse als "Lane-Länge": Team A startet nahe dem Z-Minimum, Team B nahe dem Z-Maximum, jeweils 2 Einheiten von der Wand entfernt.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-5" />
<div class="item-body">
<span class="ln">Start() — Templates pro Team bauen</span>
<pre class="code">templatesTeam0 = new Entity[unitTypes.Length];
templatesTeam1 = new Entity[unitTypes.Length];
for (int i = 0; i < unitTypes.Length; i++)
{
...
templatesTeam0[i] = CreateUnitTemplate(unitTypes[i], i, 0, team1BasePos);
templatesTeam1[i] = CreateUnitTemplate(unitTypes[i], i, 1, team0BasePos);
}</pre>
<p class="explain">Für jeden Unit-Typ werden ZWEI Templates gebaut — eines für Team 0 (das Richtung Team-1-Base läuft) und eines für Team 1 (Richtung Team-0-Base). Vorher wird geprüft, ob Definition/Mesh/Material gesetzt sind (Zeilen davor im Original), sonst Abbruch mit klarer Fehlermeldung.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-6" />
<div class="item-body">
<span class="ln">ComputeLaneBounds()</span>
<pre class="code">var walls = GameObject.FindGameObjectsWithTag(wallTag);
...
foreach (var wall in walls) { ... combined.Encapsulate(rend.bounds); }
return combined;</pre>
<p class="explain">1:1 dasselbe Muster wie <code>PilleDotsBootstrap.ComputeWallBounds()</code> — bewusst dupliziert statt geteilt, weil beide Systeme unabhängig voneinander bleiben sollen (kein gemeinsamer Code, der beide gleichzeitig beeinflussen könnte).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-7" />
<div class="item-body">
<span class="ln">CreateUnitTemplate() — Entity + Rendering</span>
<pre class="code">var entity = entityManager.CreateEntity(typeof(Team), typeof(CombatStats), typeof(Health), ...);
var desc = new RenderMeshDescription(...);
var renderMeshArray = new RenderMeshArray(new[] { def.material }, new[] { def.mesh });
RenderMeshUtility.AddComponents(entity, entityManager, desc, renderMeshArray, ...);</pre>
<p class="explain">Erstellt eine Entity mit ALLEN Komponenten, die eine Kampf-Unit braucht (Team, Kampfwerte, Leben, Cooldown, Ziel, Typ-ID, Laufziel, Transform), dann exakt dasselbe Rendering-Setup wie beim Pille-Template — nur diesmal mit dem Mesh/Material aus der jeweiligen <code>UnitDefinition</code>.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-8" />
<div class="item-body">
<span class="ln">CreateUnitTemplate() — Startwerte + Prefab-Tag</span>
<pre class="code">entityManager.SetComponentData(entity, new Team { Value = team });
entityManager.SetComponentData(entity, new CombatStats { MoveSpeed = def.moveSpeed, ... });
entityManager.SetComponentData(entity, new Health { Current = def.maxHealth, Max = def.maxHealth });
...
entityManager.SetComponentData(entity, new LaneMoveTarget { Value = enemyBasePos });
entityManager.AddComponent&lt;Prefab&gt;(entity);</pre>
<p class="explain">Kopiert die Werte aus der <code>UnitDefinition</code> in die ECS-Komponenten (das ist der "Bake"-Moment aus dem Plan). <code>LaneMoveTarget</code> wird schon HIER korrekt gesetzt (Team 0 bekommt <code>team1BasePos</code> als Ziel) — jede spätere Kopie erbt das automatisch, ohne dass man es beim Spawnen erneut setzen muss. Der <code>Prefab</code>-Tag schließt das Template wieder von allen Queries aus.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-9" />
<div class="item-body">
<span class="ln">Update()</span>
<pre class="code">if (Input.GetKeyDown(KeyCode.Alpha1)) SpawnUnit(0, 0);
if (Input.GetKeyDown(KeyCode.Alpha2)) SpawnUnit(0, 1);
if (Input.GetKeyDown(KeyCode.Alpha3)) SpawnWave(debugWaveCountPerTeam);</pre>
<p class="explain"><code>GetKeyDown</code> (nicht <code>GetKey</code>) — reagiert nur auf den Moment des Drückens, kein Dauerfeuer beim Halten. Taste 1/2 spawnen immer Unit-Typ 0 (den ersten im Array); für gezielte Typ-Auswahl kommt erst mit der Shop-UI in Phase 3 eine echte Steuerung.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-10" />
<div class="item-body">
<span class="ln">SpawnUnit()</span>
<pre class="code">var template = team == 0 ? templatesTeam0[unitTypeIndex] : templatesTeam1[unitTypeIndex];
var basePos = team == 0 ? team0BasePos : team1BasePos;
Entity newUnit = entityManager.Instantiate(template);
float offsetX = UnityEngine.Random.Range(-spawnSpreadX, spawnSpreadX);
entityManager.SetComponentData(newUnit, LocalTransform.FromPosition(basePos + new float3(offsetX, 0, 0)));</pre>
<p class="explain">Wählt Template und Basis-Position je nach Team, erzeugt die Kopie, gibt ihr eine zufällige seitliche Streuung (X-Achse) und setzt die Start-Position. <code>LaneMoveTarget</code> muss NICHT erneut gesetzt werden — kommt schon korrekt aus dem Template mit.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-11" />
<div class="item-body">
<span class="ln">SpawnWave()</span>
<pre class="code">for (int i = 0; i < countPerTeam; i++)
{
int unitTypeIndex = UnityEngine.Random.Range(0, unitTypes.Length);
SpawnUnit(unitTypeIndex, 0);
SpawnUnit(unitTypeIndex, 1);
}</pre>
<p class="explain">Spawnt <code>countPerTeam</code> Paare — pro Durchlauf einen zufälligen Unit-Typ für beide Teams gleichzeitig, damit die Test-Welle gemischt und für beide Seiten gleich stark ist.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-12" />
<div class="item-body">
<span class="ln">OnGUI()</span>
<pre class="code">int alive = aliveQuery.CalculateEntityCount();
GUI.DrawTexture(new Rect(5, 100, 430, 65), backgroundTexture);
GUI.Label(..., $"Skirmish - lebende Units: {alive} | FPS: ...");</pre>
<p class="explain">Zählt aktuell lebende Units (Query auf <code>Team</code> + <code>Health</code>) und zeichnet das Overlay bei Y=100 — bewusst UNTER dem Pille-Overlay (das bei Y=590 liegt), damit beide gleichzeitig lesbar sind, falls beide Systeme parallel in derselben Szene laufen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="skboot-13" />
<div class="item-body">
<span class="ln">Update für Phase 3: SpawnUnitInternal()</span>
<pre class="code">void SpawnUnit(int unitTypeIndex, byte team)
{
float offsetX = UnityEngine.Random.Range(-spawnSpreadX, spawnSpreadX);
SpawnUnitInternal(unitTypeIndex, team, offsetX);
}
public void SpawnUnitAt(int unitTypeIndex, byte team, Vector3 gridWorldPos)
{
SpawnUnitInternal(unitTypeIndex, team, gridWorldPos.x);
}
void SpawnUnitInternal(int unitTypeIndex, byte team, float x)
{
...
float3 spawnPos = new float3(x, basePos.y, basePos.z);
entityManager.SetComponentData(newUnit, LocalTransform.FromPosition(spawnPos));
}</pre>
<p class="explain">Für Phase 3 wurde die alte <code>SpawnUnit</code>-Methode in drei Teile aufgeteilt: die ursprüngliche Debug-Variante (zufällige X-Streuung, für Tasten 1/2/3) bleibt intern, dazu kommt eine neue <strong>öffentliche</strong> Methode <code>SpawnUnitAt</code>, die <code>WaveManager</code> aufruft — sie übernimmt die X-Position aus der Base-Grid-Platzierung, aber Y/Z immer vom festen Lane-Eingang des Teams. Ergebnis: <strong>welche Spalte im Base-Grid</strong> du befüllst, bestimmt, in welcher "Spur" der Lane die Unit lostäuft.</p>
</div>
</li>
</ul>
</article>
<!-- ====================== SYSTEM: BASE / PLACEMENT ====================== -->
<section class="system-header" id="base-section">
<h2>System 3 — Base / Placement (Phase 3)</h2>
<p>Vor-Runden-Platzierung: Units kaufen, per Klick aufs eigene Raster stellen. Komplett außerhalb ECS (MonoBehaviour), weil hier nur ein paar Dutzend Einträge pro Spieler verwaltet werden — kein Performance-Grund für DOTS. 5 Dateien.</p>
</section>
<article id="f-base-economy" class="file-card baseui">
<div class="file-head">
<span class="file-tag baseui">MonoBehaviour (Daten)</span>
<h3>Base/PlayerEconomy.cs</h3>
<div class="purpose">Ein simples Gold-Konto pro Team.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="bpe-1" />
<div class="item-body">
<span class="ln">Felder + Property</span>
<pre class="code">[SerializeField] private int startingGold = 100;
[SerializeField] private int incomePerWave = 50;
public int Gold { get; private set; }</pre>
<p class="explain"><code>{ get; private set; }</code> ist eine C#-"Property": von außen kann jeder <code>Gold</code> LESEN, aber nur die Klasse selbst darf es ÄNDERN (über <code>Spend</code>/<code>AddWaveIncome</code>) — verhindert, dass z.B. <code>ShopUI</code> aus Versehen direkt am Kontostand herumschraubt.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bpe-2" />
<div class="item-body">
<span class="ln">Awake()</span>
<pre class="code">void Awake()
{
Gold = startingGold;
}</pre>
<p class="explain"><code>Awake</code> läuft VOR jedem <code>Start</code> im ganzen Spiel — wichtig, weil andere Scripts (z.B. <code>ShopUI</code>) in ihrem eigenen <code>Start</code> schon einen gültigen <code>Gold</code>-Wert lesen können müssen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bpe-3" />
<div class="item-body">
<span class="ln">Spend()</span>
<pre class="code">public bool Spend(int amount)
{
if (amount > Gold) return false;
Gold -= amount;
return true;
}</pre>
<p class="explain">Gibt <code>true</code>/<code>false</code> zurück, statt einfach den Kontostand negativ werden zu lassen — der Aufrufer (<code>PlacementController</code>) muss das Ergebnis prüfen und die Platzierung abbrechen, falls <code>false</code>.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bpe-4" />
<div class="item-body">
<span class="ln">AddWaveIncome()</span>
<pre class="code">public void AddWaveIncome()
{
Gold += incomePerWave;
}</pre>
<p class="explain">Wird von <code>WaveManager</code> bei jedem Wellenstart aufgerufen — regelmäßiges Einkommen, unabhängig davon, was gekauft wurde.</p>
</div>
</li>
</ul>
</article>
<article id="f-base-grid" class="file-card baseui">
<div class="file-head">
<span class="file-tag baseui">MonoBehaviour (Daten + Gizmo)</span>
<h3>Base/BaseGrid.cs</h3>
<div class="purpose">Das Platzierungsraster für ein Team: welche Zelle enthält welchen Unit-Typ, plus einfache Vorschau-Objekte (kein ECS).</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="bg-1" />
<div class="item-body">
<span class="ln">Felder + Awake()</span>
<pre class="code">[SerializeField] private int columns = 6;
[SerializeField] private int rows = 4;
[SerializeField] private float cellSize = 2f;
private int[] occupiedUnitTypeIndex;
private GameObject[] previewObjects;
void Awake()
{
int cellCount = columns * rows;
occupiedUnitTypeIndex = new int[cellCount];
previewObjects = new GameObject[cellCount];
for (int i = 0; i < cellCount; i++)
occupiedUnitTypeIndex[i] = -1;
}</pre>
<p class="explain">Ein flaches 1D-Array statt eines 2D-Arrays — Zelle (Spalte, Reihe) wird über <code>row * columns + col</code> in einen einzigen Index umgerechnet (einfacher zu handhaben als ein echtes 2D-Array in C#). Alle Zellen starten als <code>-1</code> = leer.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bg-2" />
<div class="item-body">
<span class="ln">TryGetCellFromRay() — Ebene statt Collider</span>
<pre class="code">var plane = new Plane(Vector3.up, transform.position);
if (!plane.Raycast(ray, out float distance))
return false;
Vector3 hitPoint = ray.GetPoint(distance);</pre>
<p class="explain">Statt einen echten Boden-Collider zu verlangen (mehr Editor-Aufwand für dich), wird eine reine <em>mathematische</em> horizontale Ebene auf Höhe des Grid-GameObjects aufgespannt. <code>Plane.Raycast</code> berechnet, wo ein Strahl (vom Mauszeiger durch die Kamera) diese gedachte Ebene schneidet.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bg-3" />
<div class="item-body">
<span class="ln">TryGetCellFromRay() — Weltposition zu Zelle</span>
<pre class="code">Vector3 local = hitPoint - transform.position;
int col = Mathf.FloorToInt(local.x / cellSize);
int row = Mathf.FloorToInt(local.z / cellSize);
if (col < 0 || col >= columns || row < 0 || row >= rows)
return false;
cellIndex = row * columns + col;</pre>
<p class="explain">Rechnet den Treffpunkt relativ zum Grid-Ursprung um, teilt durch die Zellgröße und rundet ab (<code>FloorToInt</code>) — das ergibt Spalte/Reihe. Liegt der Punkt außerhalb der Grid-Abmessungen, wird <code>false</code> zurückgegeben (Klick daneben zählt nicht).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bg-4" />
<div class="item-body">
<span class="ln">IsOccupied() / Place()</span>
<pre class="code">public bool IsOccupied(int cellIndex) => occupiedUnitTypeIndex[cellIndex] != -1;
public void Place(int cellIndex, int unitTypeIndex, UnitDefinition def)
{
occupiedUnitTypeIndex[cellIndex] = unitTypeIndex;
...
var preview = new GameObject($"Preview_{cellIndex}_{def.unitId}");
preview.transform.SetParent(transform);
preview.transform.position = GetCellWorldPos(cellIndex);
var meshFilter = preview.AddComponent&lt;MeshFilter&gt;();
meshFilter.mesh = def.mesh;
var meshRenderer = preview.AddComponent&lt;MeshRenderer&gt;();
meshRenderer.material = def.material;</pre>
<p class="explain"><code>IsOccupied</code> nutzt eine "Expression-bodied"-Schreibweise (<code>=&gt;</code> statt <code>{ return ...; }</code>) für eine einzeilige Methode. <code>Place</code> merkt sich den Typ in der Zelle UND erzeugt ein sichtbares, aber komplett dummes Vorschau-GameObject (nur Mesh+Material, keine Scripts, keine Collider, keine ECS-Beteiligung) am Zellmittelpunkt.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bg-5" />
<div class="item-body">
<span class="ln">Clear()</span>
<pre class="code">public void Clear(int cellIndex)
{
occupiedUnitTypeIndex[cellIndex] = -1;
if (previewObjects[cellIndex] != null)
{
Destroy(previewObjects[cellIndex]);
previewObjects[cellIndex] = null;
}
}</pre>
<p class="explain">Macht eine Zelle wieder leer und entfernt das Vorschau-Objekt. Aktuell von keinem anderen Script aufgerufen — vorbereitet für später (z.B. Rechtsklick zum Entfernen).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bg-6" />
<div class="item-body">
<span class="ln">GetCellWorldPos()</span>
<pre class="code">public Vector3 GetCellWorldPos(int cellIndex)
{
int col = cellIndex % columns;
int row = cellIndex / columns;
return transform.position + new Vector3((col + 0.5f) * cellSize, 0f, (row + 0.5f) * cellSize);
}</pre>
<p class="explain">Die Umkehrung der Index-Berechnung von oben (<code>%</code> = Rest der Division = Spalte, ganzzahlige Division = Reihe), plus <code>+0.5f</code>, damit die Position die MITTE der Zelle trifft statt die Ecke.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bg-7" />
<div class="item-body">
<span class="ln">GetPlacedUnits()</span>
<pre class="code">public IEnumerable&lt;(int unitTypeIndex, Vector3 worldPos)&gt; GetPlacedUnits()
{
for (int i = 0; i < occupiedUnitTypeIndex.Length; i++)
{
if (occupiedUnitTypeIndex[i] != -1)
yield return (occupiedUnitTypeIndex[i], GetCellWorldPos(i));
}
}</pre>
<p class="explain"><code>yield return</code> macht diese Methode zu einem "Generator": statt eine ganze Liste auf einmal zu bauen, gibt sie ihre Ergebnisse nach und nach heraus, während <code>WaveManager</code> sie mit <code>foreach</code> durchläuft. Das Tupel <code>(int, Vector3)</code> bündelt zwei Werte, ohne dafür eine eigene Klasse schreiben zu müssen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bg-8" />
<div class="item-body">
<span class="ln">OnDrawGizmos()</span>
<pre class="code">void OnDrawGizmos()
{
Gizmos.color = Color.yellow;
for (int r = 0; r <= rows; r++) { Gizmos.DrawLine(...); }
for (int c = 0; c <= columns; c++) { Gizmos.DrawLine(...); }
}</pre>
<p class="explain">Zeichnet ein gelbes Liniengitter NUR im Scene-View des Editors (nie im echten Spiel/Build) — rein zur Orientierung, damit du siehst, wo genau dein Grid liegt und wie groß es ist, ohne Play drücken zu müssen.</p>
</div>
</li>
</ul>
</article>
<article id="f-base-shopui" class="file-card baseui">
<div class="file-head">
<span class="file-tag baseui">MonoBehaviour (IMGUI)</span>
<h3>Base/ShopUI.cs</h3>
<div class="purpose">Der Kaufbalken am unteren Bildschirmrand. Bewusst OnGUI statt uGUI-Canvas — passt zum Rest des Projekts, braucht keine von Hand gebauten Button-Prefabs.</div>
</div>
<div class="callout"><strong>Abweichung vom ursprünglichen Plan:</strong> Der Implementierungsplan sah ein uGUI-Canvas vor. Umgesetzt wurde stattdessen OnGUI (wie bei Pille/Skirmish-Overlays) — weniger Editor-Fummelarbeit (kein Canvas/EventSystem/Button-Prefab nötig), dafür optisch schlichter. Kann später gegen echtes uGUI getauscht werden, ohne die Logik in <code>PlacementController</code> anzufassen.</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="bshop-1" />
<div class="item-body">
<span class="ln">Felder</span>
<pre class="code">[SerializeField] private PlayerEconomy economy;
[SerializeField] private PlacementController placementController;
[SerializeField] private UnitDefinition[] shopUnits;
private Rect shopBarScreenRect;
private const float BarHeight = 90f;</pre>
<p class="explain"><code>shopBarScreenRect</code> wird jeden Frame in <code>OnGUI</code> neu berechnet und hier zwischengespeichert, damit <code>IsPointerOverUI</code> (unten) darauf zugreifen kann, ohne die Berechnung zu duplizieren.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bshop-2" />
<div class="item-body">
<span class="ln">OnGUI() — Balken + Gold</span>
<pre class="code">float barY = Screen.height - BarHeight;
shopBarScreenRect = new Rect(0, barY, Screen.width, BarHeight);
GUI.Box(shopBarScreenRect, GUIContent.none);
GUI.Label(new Rect(10, barY + 4, 200, 22), $"Gold: {(economy != null ? economy.Gold : 0)}");</pre>
<p class="explain">GUI-Koordinaten haben Y=0 OBEN — <code>Screen.height - BarHeight</code> platziert den Balken also am unteren Rand. Der <code>?:</code>-Ausdruck verhindert einen Absturz, falls <code>economy</code> im Inspector vergessen wurde (zeigt dann einfach 0 an).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bshop-3" />
<div class="item-body">
<span class="ln">OnGUI() — Buttons pro Unit-Typ</span>
<pre class="code">float x = 10;
for (int i = 0; i < shopUnits.Length; i++)
{
var def = shopUnits[i];
bool isSelected = placementController != null && placementController.SelectedUnitIndex == i;
string label = $"{def.unitId}\n{def.cost}g" + (isSelected ? " " : "");
if (GUI.Button(new Rect(x, barY + 28, ButtonWidth - 8, 54), label) && placementController != null)
placementController.SelectUnit(i);
x += ButtonWidth;
}</pre>
<p class="explain"><code>GUI.Button(...)</code> gibt <code>true</code> zurück, GENAU in dem Frame, in dem er angeklickt wurde — dann wird <code>SelectUnit(i)</code> auf dem <code>PlacementController</code> aufgerufen. Ein Häkchen (✓) markiert den gerade ausgewählten Button.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bshop-4" />
<div class="item-body">
<span class="ln">IsPointerOverUI()</span>
<pre class="code">public bool IsPointerOverUI(Vector3 mouseScreenPos)
{
Vector2 guiPoint = new Vector2(mouseScreenPos.x, Screen.height - mouseScreenPos.y);
return shopBarScreenRect.Contains(guiPoint);
}</pre>
<p class="explain">Wichtiger Kniff: <code>Input.mousePosition</code> hat Y=0 UNTEN, GUI-<code>Rect</code>s haben Y=0 OBEN — zwei verschiedene Koordinatensysteme. Diese Methode rechnet um und prüft, ob der Mausklick über dem Shop-Balken war. <code>PlacementController</code> fragt das vor jedem Platzierungs-Klick ab, damit ein Klick auf einen Shop-Button NICHT gleichzeitig eine Unit auf dem 3D-Grid platziert.</p>
</div>
</li>
</ul>
</article>
<article id="f-base-placement" class="file-card baseui">
<div class="file-head">
<span class="file-tag baseui">MonoBehaviour (Verbindung)</span>
<h3>Base/PlacementController.cs</h3>
<div class="purpose">Verbindet Shop-Auswahl → Klick aufs Grid → Gold-Abzug → tatsächliche Platzierung.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="bpc-1" />
<div class="item-body">
<span class="ln">SelectedUnitIndex + SelectUnit()</span>
<pre class="code">public int SelectedUnitIndex { get; private set; } = -1;
public void SelectUnit(int index)
{
SelectedUnitIndex = index;
}</pre>
<p class="explain">-1 bedeutet "nichts ausgewählt". <code>ShopUI</code> ruft <code>SelectUnit</code> beim Klick auf einen Button auf — das ist die einzige Möglichkeit, wie sich <code>SelectedUnitIndex</code> von außen ändert (dank <code>private set</code>).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bpc-2" />
<div class="item-body">
<span class="ln">Update() — frühe Abbrüche</span>
<pre class="code">if (SelectedUnitIndex < 0) return;
if (!Input.GetMouseButtonDown(0)) return;
if (shopUI != null && shopUI.IsPointerOverUI(Input.mousePosition)) return;</pre>
<p class="explain">Drei Gründe, sofort abzubrechen: keine Unit ausgewählt, kein frischer Linksklick in diesem Frame, oder der Klick war über dem Shop-Balken (siehe <code>ShopUI.IsPointerOverUI</code>).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bpc-3" />
<div class="item-body">
<span class="ln">Update() — Raycast + Zell-Ermittlung</span>
<pre class="code">var cam = targetCamera != null ? targetCamera : Camera.main;
if (cam == null || baseGrid == null) return;
Ray ray = cam.ScreenPointToRay(Input.mousePosition);
if (!baseGrid.TryGetCellFromRay(ray, out int cellIndex)) return;
if (baseGrid.IsOccupied(cellIndex)) return;</pre>
<p class="explain"><code>targetCamera != null ? targetCamera : Camera.main</code> — nutzt eine im Inspector zugewiesene Kamera, fällt sonst auf die als "MainCamera" getaggte Kamera zurück. <code>ScreenPointToRay</code> wandelt die 2D-Mausposition in einen 3D-Strahl durchs Spielfeld um. Ist die Zelle schon belegt, wird ebenfalls abgebrochen (kein Überschreiben).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bpc-4" />
<div class="item-body">
<span class="ln">Update() — Bezahlen + Platzieren</span>
<pre class="code">var def = shopUnits[SelectedUnitIndex];
if (economy != null && !economy.Spend(def.cost))
{
Debug.Log("⚠️ Nicht genug Gold!");
return;
}
baseGrid.Place(cellIndex, SelectedUnitIndex, def);</pre>
<p class="explain">Erst wird versucht zu bezahlen (<code>Spend</code> gibt <code>false</code> zurück, wenn zu wenig Gold da ist) — erst wenn das klappt, wird tatsächlich platziert. Bezahlt wird also NUR bei erfolgreicher Platzierung, nie vorher schon.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bpc-5" />
<div class="item-body">
<span class="ln">OnGUI()</span>
<pre class="code">if (SelectedUnitIndex < 0) return;
GUI.Label(new Rect(15, Screen.height - 120, 420, 22),
$"Ausgewählt: {shopUnits[SelectedUnitIndex].unitId} Klick auf das Grid zum Platzieren");</pre>
<p class="explain">Ein kleiner Hinweistext, der nur erscheint, solange gerade eine Unit zum Platzieren ausgewählt ist — reines Feedback, keine Interaktion.</p>
</div>
</li>
</ul>
</article>
<article id="f-base-wavemanager" class="file-card baseui">
<div class="file-head">
<span class="file-tag baseui">MonoBehaviour (Brücke, Timer)</span>
<h3>Base/WaveManager.cs</h3>
<div class="purpose">Der Wellen-Takt: alle 2040 Sekunden werden die aktuell platzierten Units beider Teams als ECS-Entities aufs Schlachtfeld gebracht.</div>
</div>
<ul class="item-list">
<li class="item">
<input type="checkbox" data-key="bwm-1" />
<div class="item-body">
<span class="ln">Start()</span>
<pre class="code">interval = Mathf.Clamp(40f - 5f * (playerCount - 1), 20f, 40f);
timer = interval;</pre>
<p class="explain">Die Formel aus dem Implementierungsplan: bei 1 Spieler 40s, pro zusätzlichem Spieler 5s weniger, aber nie unter 20s (<code>Mathf.Clamp</code> deckelt nach unten UND oben).</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bwm-2" />
<div class="item-body">
<span class="ln">Update()</span>
<pre class="code">timer -= Time.deltaTime;
if (timer <= 0f)
{
TriggerWave();
timer = interval;
}</pre>
<p class="explain">Ein klassischer Countdown-Timer: jeden Frame etwas Zeit abziehen, bei Erreichen von 0 die Welle auslösen und den Timer für die nächste Runde zurücksetzen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bwm-3" />
<div class="item-body">
<span class="ln">TriggerWave()</span>
<pre class="code">SpawnFromGrid(team0Grid, 0);
SpawnFromGrid(team1Grid, 1);
if (team0Economy != null) team0Economy.AddWaveIncome();
if (team1Economy != null) team1Economy.AddWaveIncome();</pre>
<p class="explain">Spawnt für beide Teams (Reihenfolge egal, beide passieren im selben Frame) und zahlt danach beiden Spielern ihr Wellen-Einkommen aus. Wichtig: <strong>das Base-Grid wird nicht geleert</strong> — dieselbe Platzierung wird bei der nächsten Welle wieder verwendet, ganz wie im Plan vorgesehen.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bwm-4" />
<div class="item-body">
<span class="ln">SpawnFromGrid()</span>
<pre class="code">foreach (var (unitTypeIndex, worldPos) in grid.GetPlacedUnits())
{
bootstrap.SpawnUnitAt(unitTypeIndex, team, worldPos);
}</pre>
<p class="explain">Läuft durch alle belegten Zellen (dank <code>GetPlacedUnits()</code>s <code>yield return</code> ohne vorher eine Liste zu bauen) und ruft für jede eine echte ECS-Entity-Erzeugung über <code>SkirmishBootstrap.SpawnUnitAt</code> auf — das ist die einzige Stelle in diesen 5 Dateien, wo überhaupt mit ECS "in Kontakt" gekommen wird, und selbst hier nur indirekt über die öffentliche Bootstrap-Methode, nie direkt mit <code>Entity</code>/<code>EntityManager</code>.</p>
</div>
</li>
<li class="item">
<input type="checkbox" data-key="bwm-5" />
<div class="item-body">
<span class="ln">OnGUI()</span>
<pre class="code">GUI.Label(new Rect(15, 160, 400, 22), $"Nächste Welle in: {Mathf.CeilToInt(timer)}s");</pre>
<p class="explain"><code>CeilToInt</code> (aufrunden) statt <code>RoundToInt</code>, damit die Anzeige nicht schon bei z.B. 0.4s Restzeit "0s" zeigt, sondern erst wenn die Welle wirklich ausgelöst wurde.</p>
</div>
</li>
</ul>
</article>
<div style="height: 40px;"></div>
</main>
</div>
<script>
(function () {
var checkboxes = document.querySelectorAll('input[type="checkbox"][data-key]');
checkboxes.forEach(function (cb) {
var storeKey = 'hf-doc-' + cb.dataset.key;
cb.checked = localStorage.getItem(storeKey) === '1';
syncRowState(cb);
cb.addEventListener('change', function () {
localStorage.setItem(storeKey, cb.checked ? '1' : '0');
syncRowState(cb);
updateProgress();
});
});
function syncRowState(cb) {
var row = cb.closest('.item') || cb.closest('.step');
if (row) row.classList.toggle('checked', cb.checked);
}
function updateProgress() {
var all = document.querySelectorAll('input[type="checkbox"][data-key]');
var checked = document.querySelectorAll('input[type="checkbox"][data-key]:checked');
var pct = all.length ? Math.round((checked.length / all.length) * 100) : 0;
document.getElementById('progress-fill').style.width = pct + '%';
document.getElementById('progress-text').textContent = checked.length + ' / ' + all.length + ' verstanden (' + pct + '%)';
}
updateProgress();
})();
</script>