2354 lines
125 KiB
HTML
2354 lines
125 KiB
HTML
|
|
<title>Happy Forest — Code-Dokumentation & 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 20–40s).</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. 1–2</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. 1–2</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. 6–7 (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. 10–11</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. 1–4</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. 10–11</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. 13–16</span>
|
|||
|
|
<pre class="code">public void OnCreate(ref SystemState state)
|
|||
|
|
{
|
|||
|
|
state.RequireForUpdate<PilleSimulationConfig>();
|
|||
|
|
}</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. 21–22</span>
|
|||
|
|
<pre class="code">var config = SystemAPI.GetSingleton<PilleSimulationConfig>();
|
|||
|
|
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. 24–30</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. 39–42</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. 46–47</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. 51–55</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. 57–61</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. 63–64</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. 1–6</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. 10–13 (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. 23–25</span>
|
|||
|
|
<pre class="code">var config = SystemAPI.GetSingleton<PilleSimulationConfig>();
|
|||
|
|
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. 27–30</span>
|
|||
|
|
<pre class="code">var query = SystemAPI.QueryBuilder().WithAll<PilleComponent, LocalTransform>().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. 32–33</span>
|
|||
|
|
<pre class="code">var transforms = query.ToComponentDataArray<LocalTransform>(Allocator.TempJob);
|
|||
|
|
var pilles = query.ToComponentDataArray<PilleComponent>(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. 37–42</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. 44–45</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. 47–50</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. 54–59</span>
|
|||
|
|
<pre class="code">[BurstCompile]
|
|||
|
|
public struct ResolvePairCollisionsJob : IJob
|
|||
|
|
{
|
|||
|
|
[ReadOnly] public NativeArray<LocalTransform> Transforms;
|
|||
|
|
public NativeArray<PilleComponent> 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. 63–66</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. 68–71</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 > 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. 75–82</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. 1–7</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. 17–26</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. 28–37</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. 41–46</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. 50–58</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. 63–74 — 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. 77–100 — 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. 104–107 — 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. 109–117 — 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. 119–126 — 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. 128–131 — 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<Prefab>(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. 134–160 — 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. 162–173 — 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<LocalTransform>(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. 175–187 — 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. 190–199 — 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. 201–211 — ToggleCollision()</span>
|
|||
|
|
<pre class="code">var configEntity = configQuery.GetSingletonEntity();
|
|||
|
|
var cfg = entityManager.GetComponentData<PilleSimulationConfig>(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. 213–219 — 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. 221–230 — 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. 232–236 — 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 3–5).</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. 3–14</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. 20–21</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. 24–30 (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. 32–34 (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<DeadTag></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<float3></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<CombatStats>();
|
|||
|
|
}</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<int, Entity> 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<LaneGridSingleton>())
|
|||
|
|
{
|
|||
|
|
var entity = state.EntityManager.CreateEntity(typeof(LaneGridSingleton));
|
|||
|
|
state.EntityManager.SetComponentData(entity, new LaneGridSingleton
|
|||
|
|
{
|
|||
|
|
Buckets = new NativeParallelMultiHashMap<int, Entity>(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<LaneGridSingleton>())
|
|||
|
|
{
|
|||
|
|
var grid = SystemAPI.GetSingleton<LaneGridSingleton>();
|
|||
|
|
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<LaneGridSingleton>();
|
|||
|
|
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<LaneGridSingleton>();
|
|||
|
|
var job = new AcquireTargetJob { Buckets = grid.Buckets, ..., TeamLookup = SystemAPI.GetComponentLookup<Team>(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<EndSimulationEntityCommandBufferSystem.Singleton>()
|
|||
|
|
.CreateCommandBuffer(state.WorldUnmanaged);
|
|||
|
|
var healthLookup = SystemAPI.GetComponentLookup<Health>();</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<RefRO<Target>, RefRO<CombatStats>, RefRW<AttackCooldownTimer>>()
|
|||
|
|
.WithNone<DeadTag>())</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<DeadTag>(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<DeadTag>();</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<EndSimulationEntityCommandBufferSystem.Singleton>()
|
|||
|
|
.CreateCommandBuffer(state.WorldUnmanaged);
|
|||
|
|
foreach (var (deadTag, entity) in SystemAPI.Query<RefRO<DeadTag>>().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<Prefab>(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=5–90 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<MeshFilter>();
|
|||
|
|
meshFilter.mesh = def.mesh;
|
|||
|
|
var meshRenderer = preview.AddComponent<MeshRenderer>();
|
|||
|
|
meshRenderer.material = def.material;</pre>
|
|||
|
|
<p class="explain"><code>IsOccupied</code> nutzt eine "Expression-bodied"-Schreibweise (<code>=></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<(int unitTypeIndex, Vector3 worldPos)> 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 20–40 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>
|