singe/docs/de/lessons/26-navigation.adoc

588 lines
26 KiB
Text

[#_lesson_26_characters_that_move_themselves]
== Lektion 26: Figuren, die sich selbst bewegen
image::learn/26-navigation.png[Die fertige Lektion, 480]
In dieser Lektion baust du einen Raum mit einer Wand quer durch die Mitte und stellst zwei Figuren
hinein. Die eine schickst du per Knopfdruck in eine Ecke. Die andere sucht sich zufällig irgendeinen
Ort aus, läuft dorthin und sucht sich, wenn sie ankommt, einen anderen aus, für immer.
Keine von beiden läuft in die Wand, und du schreibst nie eine Zeile, die sagt, wie man um sie
herumkommt. Das ist neu. Jede Lektion bis zu dieser hat Dinge dorthin bewegt, wo du sie hinhaben
wolltest. Das ist die erste Lektion, in der das Spiel selbst etwas herausfindet.
[#_the_problem_with_walking_toward_something]
=== Das Problem mit dem Laufen auf etwas zu
Hier ist der naheliegende Weg, eine Figur zu einem Punkt laufen zu lassen, und so schreibt es jeder
zuerst. Nimm das Ziel, zieh ab, wo du bist, und geh ein kleines Stück auf der Linie dazwischen, in
jedem Frame, bis du ankommst.
In einem leeren Raum funktioniert das wunderbar.
Stell eine Wand in den Raum, und es funktioniert nicht mehr, und zwar auf eine Art, die eher dumm
als kaputt aussieht: Die Figur marschiert in die Wand, bleibt dort stehen und schrammt daran, einen
Meter neben einem Durchgang, den sie nicht sehen kann, weil nichts in „geh auf das Ziel zu“ eine
Ahnung davon hat, dass es in der Welt Formen gibt.
Da kannst du dich nicht herausflicken. Wenn die Figur zur Seite abbiegt, sobald sie eine Wand
berührt, bleibt sie in der ersten Ecke hängen, die sie findet. Es reicht auch nicht, zu wissen, wo
die Wände sind, denn der Weg um eine Wand herum kann lang sein, kann sich erst vom Ziel entfernen,
bevor er zurückkommt, und kann überhaupt nicht existieren.
Was du brauchst, ist eine Möglichkeit, eine echte Frage zu stellen: *Wie laufe ich von hier nach
dort?* Um sie zu beantworten, braucht es etwas, das die Form des Bodens kennt.
[#_what_a_navigation_mesh_is]
=== Was ein Navigationsmesh ist
Stell dich in einen Raum und sieh auf den Boden. Auf manchem davon könntest du laufen. Auf manchem
nicht: Er liegt unter einem Tisch, in einer Wand, jenseits der Kante eines Balkons oder hinter einer
Stufe, die zu hoch zum Hinaufsteigen ist. Stell dir jetzt vor, du malst den Teil an, auf dem du
laufen könntest, und wirfst alles andere weg -- die Wände, die Möbel, die Decke, die Kulisse. Übrig
bleibt eine flache Form, oder mehrere, mit Löchern, wo die Säulen sind, und Lücken, wo es
hinuntergeht.
Diese angemalte Form ist ein *Navigationsmesh*, und mehr braucht eine Figur nicht, um einen Weg zu
planen. Es ist nicht das Level. Es ist eine vereinfachte Karte der Teile des Levels, auf denen ein
Mensch stehen kann, und weil es einfach ist, lassen sich Fragen dazu schnell beantworten.
Singe macht es für dich aus dem Level, das du schon hast. Du sagst, wie groß die Figuren sind,
gibst ihm die Geometrie, und es findet heraus, was begehbar ist: Böden und Rampen innerhalb der
Steigung, die du erlaubst, Stufen, die nicht höher sind, als du erlaubst, und um jede Wand und jede
Kante wird ein Rand abgeschnitten, damit eine Figur, die eine Breite hat, keine Ecke schneidet. Das
nennt man *Backen*, es passiert einmal, und das Ergebnis kann in einer Datei gespeichert und beim
nächsten Mal geladen werden.
[#_a_room_with_a_wall_in_it]
=== Ein Raum mit einer Wand darin
Leg einen Ordner namens `navigation` an und darin eine Datei namens `navigation.singe`. Dieser
Teil ist eine dreidimensionale Szene, und darum ging es in Lektion zweiundzwanzig: Eine *Szene* ist eine Welt, die in drei Dimensionen gezeichnet wird, und alles darin
hängt an einem *Knoten*, also an einer Position, einer Drehung und einer Skalierung, an die du eine
Form hängen kannst.
Wenn du diese Lektion übersprungen hast, kannst du dieser hier trotzdem folgen. Nimm die nächsten
beiden Blöcke einfach als „ein grauer Boden, zwanzig Einheiten im Quadrat, drei hellbraune Wände
darauf und eine Kamera, die auf das Ganze hinunterschaut“.
[source,lua]
----
local ROOM = 20
local WALL_HEIGHT = 2
local AGENT_RADIUS = 0.4
local AGENT_HEIGHT = 1.8
local WALK_SPEED = 2.2
sceneEnable(true)
sceneSetBackground(28, 32, 42, 255)
sceneSetAmbient(80, 86, 100)
local floorLook = materialNew()
materialSetColor(floorLook, 96, 102, 110)
materialSetRoughness(floorLook, 0.9)
local wallLook = materialNew()
materialSetColor(wallLook, 150, 128, 104)
materialSetRoughness(wallLook, 0.8)
local ground = nodeNew()
nodeSetMesh(ground, meshPlane(ROOM, ROOM), floorLook)
local wallMesh = meshBox(1, WALL_HEIGHT, 1)
local walls = {}
function addWall(x, z, width, depth)
local wall = nodeNew()
nodeSetMesh(wall, wallMesh, wallLook)
nodeSetScale(wall, width, 1, depth)
nodeSetPosition(wall, x, WALL_HEIGHT / 2, z)
walls[#walls + 1] = wall
end
addWall(-4, 0, 12, 0.6)
addWall(7.5, 0, 5, 0.6)
addWall(-3, -6, 0.6, 8)
----
Die Einheiten hier sind Meter, denn davon geht die dreidimensionale Welt von Singe aus. Die ersten
beiden Wände laufen quer durch die Mitte des Raums, mit einer Lücke von drei Metern dazwischen: ein
Durchgang. Die dritte ist ein Stummel, der in die hintere Hälfte ragt, damit die Figuren etwas
haben, um das sie herumlaufen müssen, wenn sie durch sind.
Füg ein Licht und eine Kamera hinzu, damit es etwas zu sehen gibt:
[source,lua]
----
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, 8, 14, 6)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 3.5)
lightSetShadow(sun, true)
local camera = nodeNew()
nodeSetPosition(camera, 0, 17, 19)
nodeLookAt(camera, 0, 0, 0)
cameraSet(camera)
cameraSetPerspective(55, 0.1, 120)
function onOverlayUpdate()
overlayClear()
return OVERLAY_UPDATED
end
----
Starte es mit `Singe -R navigation`. Ein grauer Boden, eine Wand mit einer Lücke und ein Stummel, von oben und von hinten
gesehen.
[#_bake_the_floor]
=== Back den Boden
Sechs Zeilen machen aus diesem Raum eine Karte:
[source,lua]
----
local nav = navNew(AGENT_RADIUS, AGENT_HEIGHT, 45, 0.3)
navAddNode(nav, ground)
for _, wall in ipairs(walls) do
navAddNode(nav, wall)
end
navBuild(nav)
----
Setz sie hinter die Wände und vor das Licht. Dann füg in `onOverlayUpdate` unter `overlayClear` eine
Zeile ein, damit du siehst, was du bekommen hast:
[source,lua]
----
navDraw(nav, 0, 170, 190)
----
Starte es noch einmal. Ein Netz aus blaugrünen Dreiecken liegt über dem Boden, und es hört vor den
Wänden auf. Die Lücke, die es lässt, ist keine Dekoration: Es sind die 0,4 Meter Abstand, die eine
Figur mit einem Radius von 0,4 Metern braucht, damit ihre Schultern nicht durch den Stein gehen. Der
Durchgang ist im Mesh immer noch ein Durchgang, denn drei Meter sind breit genug, um hindurchzugehen;
mach die Lücke einen halben Meter breit, und das Blau würde sich darüber schließen, und für jede
Figur in deinem Spiel gäbe es dort überhaupt keine Tür.
Sieh es dir einen Moment lang an, denn `navDraw` ist das Werkzeug, zu dem du jedes Mal greifen
wirst, wenn eine Figur etwas Unerklärliches tut. In neun von zehn Fällen liegt die Antwort vor dir
auf dem Boden.
[#_somebody_to_walk_it]
=== Jemand, der darauf läuft
Eine Figur ist ein Knoten. Wie sie aussieht, geht die Navigation nichts an, also bau das Billigste,
was zeigt, in welche Richtung sie schaut: einen Kasten mit einer Nase dran.
[source,lua]
----
function makeWalker(x, z, look)
local walker = nodeNew()
nodeSetPosition(walker, x, 0, z)
local body = nodeNew()
nodeSetParent(body, walker)
nodeSetMesh(body, meshBox(0.7, AGENT_HEIGHT, 0.7), look)
nodeSetPosition(body, 0, AGENT_HEIGHT / 2, 0)
local nose = nodeNew()
nodeSetParent(nose, body)
nodeSetMesh(nose, meshSphere(0.22, 16), look)
nodeSetPosition(nose, 0, 0.4, -0.45)
return walker
end
----
Das kommt zu `addWall`. Es braucht eine Farbe, also füg neben den anderen Materialien eine hinzu:
[source,lua]
----
local walkerLook = materialNew()
materialSetColor(walkerLook, 225, 85, 60)
materialSetEmissive(walkerLook, 70, 18, 10)
----
Und jetzt die Zeile, für die es diese Lektion gibt. Nach `navBuild`:
[source,lua]
----
local walker = makeWalker(-7, 7, walkerLook)
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
----
`navAgentNew` setzt den Knoten auf das Mesh und gibt einen *Agenten* zurück: das Wort der Engine für
etwas, das sie bereit ist, für dich herumlaufen zu lassen. Ab diesem Moment gehört die Position
dieses Knotens der Engine. In jedem Frame bewegt sie ihn und dreht ihn in die Richtung, in die er
geht, und das tut sie so lange, bis du ihm sagst, wohin er gehen soll, oder den Agenten wegnimmst.
Starte es. Ein roter Kasten mit einer Nase erscheint in der vorderen Ecke und tut absolut nichts,
weil du noch um nichts gebeten hast.
[#_sending_it_somewhere]
=== Ihn irgendwohin schicken
Gib ihm eine Liste von Orten und einen Knopf.
[source,lua]
----
local posts = {
{ 7, 0, 7 },
{ 7, 0, -7 },
{ -7, 0, -7 },
{ -7, 0, 7 },
{ 30, 0, 30 }
}
local post = 0
local message = "Button 1 sends the red one somewhere."
function onInputPressed(what)
if what ~= SWITCH_BUTTON1 then
return
end
post = post % #posts + 1
local target = posts[post]
if navAgentMoveTo(walkerAgent, target[1], target[2], target[3]) then
message = string.format("Walking to post %d.", post)
else
message = string.format("No path to post %d. Staying put.", post)
end
end
----
Gib die Meldung aus, damit du siehst, was passiert. In `onOverlayUpdate`, nach `navDraw`:
[source,lua]
----
overlayPrint(2, 2, message)
----
Starte es und drück Knopf eins -- standardmäßig die *Leertaste*. Der Kasten dreht sich, läuft in die
hintere Ecke seiner eigenen Raumhälfte und bleibt stehen. Drück noch einmal. Er läuft an der Wand
entlang, findet den Durchgang, geht hindurch, kommt auf der anderen Seite heraus und bleibt in der
hinteren Ecke stehen. Drück ein drittes Mal, und er geht um den Stummel herum zur vierten Ecke. Du
hast ihm nichts von dem Durchgang gesagt.
Nichts in deinem Skript erwähnt den Durchgang. Er hat ihn gefunden, weil der Durchgang ein Loch in
einer Form ist, und um Löcher herumzugehen ist genau das, wofür das Mesh da ist.
Jeder Eintrag in `posts` besteht aus drei Zahlen, einem x, einem y und einem z, weil ein Punkt in
einer dreidimensionalen Welt drei braucht. Y zeigt nach oben, und deine sind alle null, weil hier
alles auf dem Boden liegt. Die Tabelle `posts` ist eine Tabelle aus Tabellen, aus Lektion sechs,
und `posts[post]` ist eine der inneren.
[#_knowing_when_it_arrives]
=== Wissen, wann er ankommt
Der Kasten bleibt stehen, wenn er ankommt, aber dein Skript weiß nicht, dass er angekommen ist. Lass
es dir sagen:
[source,lua]
----
function onNavArrived(agent)
message = string.format("Arrived at post %d.", post)
end
----
Das ist ein Callback, dieselbe Idee wie `onOverlayUpdate` in Lektion eins: Du schreibst ihn, die
Engine ruft ihn auf. Dieser läuft einmal, in dem Moment, in dem ein Agent das Ziel erreicht, das er
bekommen hat. Nicht, wenn er in der Nähe ist, und nicht in jedem Frame danach -- einmal, bei der
Ankunft.
Er bekommt den Agenten übergeben, denn ein Spiel hat mehr als einen. Dieses Argument ist die Zahl,
die `navAgentNew` zurückgegeben hat, und indem du es vergleichst, erkennst du, welche deiner Figuren
gerade dort angekommen ist, wo sie hinwollte.
[#_when_there_is_no_path]
=== Wenn es keinen Weg gibt
Drück den Knopf ein fünftes Mal, und die Meldung lautet `No path to post 5.
Staying put.`, und der Kasten bewegt sich nicht.
Posten fünf liegt bei `{ 30, 0, 30 }`, und das ist zwanzig Meter außerhalb eines Raums, der zwanzig
Meter breit ist. Dort gibt es keinen begehbaren Boden, es gibt keine Route dorthin, und es gibt
nichts Vernünftiges, was eine Figur dagegen tun könnte. `navAgentMoveTo` sagt das, indem es `false`
zurückgibt, und es lässt den Agenten genau so, wie er war.
Das ist der Teil, den Anfänger überspringen, und es ist der Teil, der zubeißt. Echte Spiele bitten
ständig um unmögliche Wege, denn das Ziel ist meistens der Spieler, und der Spieler kommt überallhin:
Er steht auf einer Kiste, ist mitten im Sprung, im Wasser, hinter einer Tür, die zugefallen ist,
oder durch eine Lücke geschlüpft, die das Navigationsmesh als zu schmal weggeschnitten hat. Jeder
dieser Fälle lässt ein völlig vernünftiges `navAgentMoveTo` `false` zurückgeben.
Dein Skript entscheidet, was das bedeutet. Eine Wache, die dich nicht erreichen kann, sollte wieder
Streife gehen, oder zum nächsten Punkt laufen, den sie erreichen *kann*, und dort warten, oder
rufen. Was sie nicht tun darf, ist nichts, während dein Code still ein zurückgegebenes `false`
übergeht und du dich eine Stunde lang fragst, warum einer von zwanzig Gegnern erstarrt ist.
Zwei andere Aufrufe helfen hier, und es lohnt sich, sie zu kennen, bevor du sie brauchst.
`navNearest` nimmt irgendeinen Punkt und gibt die nächstgelegene Stelle auf dem Mesh zurück, oder
`nil`, wenn nichts in der Nähe ist; so machst du aus „wo der Spieler ist“ „irgendwo, wo eine Figur
wirklich stehen kann“. Und `navPath` stellt dieselbe Frage wie `navAgentMoveTo`, ohne jemanden zu
bewegen: Es gibt die Ecken der Route als Tabelle von Punkten zurück, oder `nil`, wenn es keine
Route gibt. Du kannst sie ausmessen, zeichnen oder entscheiden, dass der Weg zu lang ist, um sich
damit abzugeben.
Das Zeichnen ist der nützliche Trick. Füg das hier in `onOverlayUpdate` ein:
[source,lua]
----
local target = posts[post]
if target ~= nil then
local x, y, z = nodeGetWorldPosition(walker)
local route = navPath(nav, x, y, z, target[1], target[2], target[3])
if route ~= nil then
for i = 2, #route do
local a = route[i - 1]
local b = route[i]
lineDraw(a[1], a[2] + 0.1, a[3], b[1], b[2] + 0.1, b[3], 255, 215, 60)
end
end
end
----
Jetzt läuft eine gelbe Linie vom Kasten bis dorthin, wo er hinwill, knickt durch den Durchgang ab
und wird kürzer, während er läuft. Schick ihn zu Posten fünf, und die Linie wird gar nicht
gezeichnet, weil `navPath` `nil` zurückgegeben und das `if` es abgefangen hat. Du siehst der
Entscheidung zu, die der Agent trifft, auf den Boden gezeichnet.
[#_a_second_walker]
=== Ein zweiter Läufer
Eine Figur, die Wänden ausweicht, ist Navigation. Zwei Figuren, die Wänden *und einander*
ausweichen, sind das, was dir die Engine tatsächlich gibt, und es kostet vier Zeilen mehr.
Füg neben dem roten ein blaues Material hinzu, dann das hier unter dem ersten Agenten:
[source,lua]
----
local wanderer = makeWalker(6, -6, wandererLook)
local wandererAgent = navAgentNew(nav, wanderer, AGENT_RADIUS, AGENT_HEIGHT, 1.4)
----
Schick ihn nach der Kamera irgendwohin, damit er loslegt:
[source,lua]
----
navAgentMoveTo(wandererAgent, navRandomPoint(nav))
----
Und bring `onNavArrived` bei, die beiden auseinanderzuhalten:
[source,lua]
----
function onNavArrived(agent)
if agent == wandererAgent then
navAgentMoveTo(agent, navRandomPoint(nav))
else
message = string.format("Arrived at post %d.", post)
end
end
----
`navRandomPoint` sucht sich eine Stelle auf dem begehbaren Mesh aus -- irgendwo darauf, nie in einer
Wand -- und gibt drei Zahlen zurück, und das ist genau das, was `navAgentMoveTo` als Nächstes haben
will. Weil der Callback bei der Ankunft feuert und seine Antwort eine weitere Reise ist, wandert der
blaue Kasten jetzt durch den Raum, solange das Spiel läuft, aus vier Zeilen und ganz ohne Zustand.
Sieh zu, wie sich die beiden im Durchgang begegnen. Keiner bleibt abrupt stehen, und keiner läuft
durch den anderen hindurch: Beide weichen rechtzeitig ein wenig zur Seite aus und gehen aneinander
vorbei. Das sind die Agenten, die umeinander herumsteuern, und das ist der Unterschied zwischen
Figuren und Möbeln.
Das fertige Skript ist `learn/26-navigation.singe`.
[#_what_just_happened_22]
=== Was gerade passiert ist
[source,lua]
----
local nav = navNew(AGENT_RADIUS, AGENT_HEIGHT, 45, 0.3)
----
Beginnt ein Navigationsmesh, und die vier Zahlen sind eine Beschreibung des Menschen, für den es
gedacht ist: 0,8 Meter breit, 1,8 Meter groß, fähig, eine Steigung von bis zu fünfundvierzig Grad zu
gehen, und fähig, eine Stufe von bis zu 0,3 Metern hinaufzusteigen.
Jede dieser Zahlen ändert die Form, die herauskommt. Eine breitere Figur verliert mehr Boden an den
Rand um die Wände und findet einen Durchgang vielleicht verschlossen. Eine größere kann nicht unter
einem niedrigen Balken durchgehen. Erlaube eine steilere Steigung, und eine Rampe wird zur Straße;
erlaube weniger, und sie wird zur Klippe.
Der Radius entscheidet auch, wie fein der Boden vermessen wird, also dauert das Backen bei einem
sehr kleinen Radius über einem sehr großen Level lange. Den Eintrag zu `navNew` in der Referenz solltest
du lesen, bevor du deine Zahlen wählst.
[source,lua]
----
navAddNode(nav, ground)
navBuild(nav)
----
`navAddNode` übergibt Geometrie -- einen Knoten und alles, was unter ihm hängt, so wie es in diesem
Moment in der Welt steht, also stell die Dinge an ihren Platz, bevor du sie hinzufügst. Ein Aufruf
nimmt ein ganzes geladenes Modell. Nur echte Formen zählen: Ein Licht, ein Sound oder ein
Physikkörper für sich allein fügt nichts hinzu.
`navBuild` erledigt das Backen, und es ist der langsame Aufruf in dieser Lektion: nichts, was du
bemerken würdest, für einen Raum wie diesen, eine Zehntelsekunde für eine Kathedrale auf einem
Desktop-Rechner, mehrere Sekunden auf einem Raspberry Pi. Mach es, während ein Titelbildschirm zu
sehen ist, nicht, während der Spieler wartet.
Danach wird die Geometrie weggeworfen, und das Mesh ist geschlossen. Du kannst nichts mehr
hinzufügen. Wenn dein Level seine Form ändert -- eine Tür geht auf, eine Brücke stürzt ein -- backst
du ein anderes Mesh, und `navDelete` gibt das alte frei.
Auf einem langsamen Rechner willst du lieber gar nicht backen, also schreibt `navSave` das fertige
Mesh in eine Datei, und `navLoad` liest es wieder ein, gebaut und bereit. Back einmal auf deinem
eigenen Rechner, liefere die Datei mit dem Spiel aus, und der Spieler wartet nie.
[source,lua]
----
local walkerAgent = navAgentNew(nav, walker, AGENT_RADIUS, AGENT_HEIGHT, WALK_SPEED)
----
Ein Agent ist ein Knoten, den die Engine für dich herumlaufen lässt, weil sie sich dazu bereit
erklärt hat. Radius und Höhe benutzt sie hier, um Abstand zu anderen Agenten zu halten, und die
letzte Zahl ist seine Höchstgeschwindigkeit in Metern pro Sekunde -- 2,2 ist zügiges Gehen, 1,4 ist
Schlendern, 6 ist ein Sprint.
Setz ab jetzt die Position dieses Knotens nicht mehr selbst. Zwei Dinge, die einen Knoten bewegen,
kämpfen miteinander, und der Kampf sieht aus wie Ruckeln. Wenn deine Figur nicht nur laufen, sondern
auch von der Physik herumgeschubst werden soll, gibt es dafür einen Aufruf -- `navAgentSetPlayer`
übergibt die Steuerung stattdessen einem Physik-Controller -- und der Eintrag dazu in der Referenz
erklärt, wann du das willst.
[source,lua]
----
local ok = navAgentMoveTo(walkerAgent, x, y, z)
----
Setzt das Ziel. Der Aufruf läuft nicht selbst dorthin; er sagt, wo „dort“ ist, und die Engine
kümmert sich in den folgenden Frames darum. Du darfst ihn jederzeit erneut aufrufen, und das neue
Ziel ersetzt das alte; so folgt eine Wache einem Spieler, der rennt: Richte sie ein- oder zweimal
pro Sekunde neu aus, nicht sechzigmal.
Und er gibt `true` oder `false` zurück, und du musst nachsehen.
[source,lua]
----
navAgentStop(walkerAgent)
----
Wird oben nicht benutzt, ist aber die andere Hälfte des Paars: Es vergisst das Ziel, und der Agent
wird langsamer, bis er dort steht, wo er gerade ist. `onNavArrived` feuert nicht, weil er nicht
angekommen ist. Benutz es, wenn der Grund für die Reise weggefallen ist.
[#_fast_is_not_the_point]
=== Auf Tempo kommt es nicht an
Noch ein Aufruf, denn er macht aus einer Vorführung etwas, das wie ein Spiel aussieht.
[source,lua]
----
local vx, vy, vz = navAgentGetVelocity(walkerAgent)
local speed = math.sqrt(vx * vx + vz * vz)
overlayPrint(2, 4, string.format("Speed %.2f", speed))
----
`navAgentGetVelocity` sagt dir, wie sich der Agent gerade jetzt bewegt: nicht, wohin er geht,
sondern wie schnell und in welche Richtung er in diesem Frame tatsächlich unterwegs ist. Beobachte
die Zahl, wenn der Kasten losläuft und wenn er zum Stehen kommt. Er beschleunigt sanft und bremst
sanft.
An diese Zahl hängst du eine Animation. Eine Figur, deren Beine sich mit der Geschwindigkeit
bewegen, mit der sie wirklich unterwegs ist, wirkt lebendig; eine, deren Laufzyklus mit fester
Geschwindigkeit läuft, während sie beschleunigt, sieht aus, als liefe sie Schlittschuh. Mit einem
echten Modell statt des Kastens würdest du diese Geschwindigkeit mit ein paar Schwellenwerten
vergleichen und eine Steh-, eine Geh- oder eine Renn-Animation abspielen -- darum geht es in Lektion
dreiundzwanzig, und das Beispiel in der Referenz unter `navAgentGetVelocity` ist genau dafür
geschrieben.
Und das ist der eigentliche Punkt dieser Lektion. Eine Figur, die die schnelle gerade Linie nimmt
und an einer Wand entlangschrammt, ist schlechter als eine, die eine Sekunde später losgeht, sich
richtig dreht, außen herum durch die Tür geht und langsamer wird, wenn sie ankommt. Niemand, der ein
Spiel spielt, misst, wie schnell ein Gegner bei ihm war. Aber jeder merkt sofort, ohne sagen zu
können, warum, wenn sich etwas so bewegt, wie sich kein Lebewesen bewegen würde.
[#_try_it_26]
=== Probier es aus
. *Mach die Tür zu.* Ändere die ersten beiden Wände so, dass die Lücke zwischen ihnen einen halben
Meter breit ist statt drei: `addWall(-4, 0, 13, 0.6)` und
`addWall(6.5, 0, 7, 0.6)`. Starte es und sieh dir an, was `navDraw` zeigt, bevor du irgendetwas
drückst. Dann schick den Kasten hinüber und sieh, was er tut.
. *Mach alle dick.* Setz `AGENT_RADIUS` auf `2.0` und starte es. Der Durchgang ist immer noch drei
Meter breit, und die blauen Dreiecke sind daraus verschwunden. Nichts ist kaputt; das Mesh
beantwortet jetzt eine andere Frage.
. *Schick ihn zur Maus.* Nimm mit `sceneUnproject` den Punkt unter dem Mauszeiger, rück ihn mit
`navNearest` auf das Mesh und schick den Kasten dorthin, wenn der Knopf gedrückt wird. Dass
`navNearest` `nil` zurückgibt, ist der Fall, um den du dich kümmern musst, und er tritt immer dann
ein, wenn der Zeiger nicht über dem Boden ist.
. *Füg vier weitere Wanderer hinzu.* Pack die vier Zeilen des blauen Läufers in eine Schleife mit
`for` aus Lektion vier, bewahr die Agenten in einer Tabelle auf und lass sie alle herumwandern.
Beobachte den Durchgang, wenn drei von ihnen gleichzeitig hindurchwollen.
. *Back es einmal.* Ruf `navSave(nav, "room.nav")` nach `navBuild` auf, dann ersetze `navNew`, die
Aufrufe von `navAddNode` und `navBuild` durch ein einziges
`navLoad("room.nav", AGENT_RADIUS, AGENT_HEIGHT)`. Jetzt ändere die Position einer Wand und starte
es noch einmal. Was du siehst, ist die Falle, die mit gebackenen Meshes kommt, und es lohnt sich,
ihr einmal absichtlich zu begegnen.
[#_break_it_on_purpose_25]
=== Mach es absichtlich kaputt
Backen schließt das Mesh. Versuch, nachträglich noch etwas hinzuzufügen -- eine Kiste, die du spät
hingestellt hast, oder eine Wand, die du vergessen hast -- indem du diese Zeile direkt hinter
`navBuild(nav)` setzt:
[source,lua]
----
navAddNode(nav, ground)
----
Singe hält an, bevor das Spiel startet, und nennt die Zeile in deiner Datei, in der dieser
Aufruf steht:
----
68:navAddNode: The mesh is already built.
----
Ein Fehler der Engine hat die Form: die Zeile, der Aufruf, der sich gewehrt hat, und die
Beschwerde, und diese Beschwerde meint, was sie sagt: `navBuild` hat die Geometrie verbraucht, die
es bekommen hat, und es gibt nichts mehr, wozu man etwas hinzufügen könnte.
Die Lösung ist nie, den Aufruf von `navAddNode` zu verschieben. Sie ist, zu bemerken, dass du
Geometrie für zwei Level hast und zwei Meshes willst, oder dass du zu früh gebacken hast. Füg alles
hinzu, dann bau, und bau zuletzt.
[#_what_you_learned_26]
=== Was du gelernt hast
* Auf ein Ziel zuzugehen funktioniert in einem leeren Raum und versagt, sobald es eine Wand gibt,
und kein noch so eifriges Flicken behebt das.
* Ein Navigationsmesh ist eine vereinfachte Karte des Bodens, auf dem eine Figur stehen kann, ohne
die Wände, die Abgründe und den Abstand um sie herum.
* `navNew` beschreibt die Figur, für die das Mesh ist, `navAddNode` übergibt die Geometrie des
Levels, und `navBuild` backt es. Nach dem Backen kann nichts mehr hinzugefügt werden.
* `navSave` und `navLoad` bewahren ein gebackenes Mesh in einer Datei auf, damit der Spieler nie
darauf wartet.
* `navDraw` zeigt dir das Mesh, und es ist das Erste, was du dir ansiehst, wenn sich eine Figur
seltsam verhält.
* `navAgentNew` gibt der Engine einen Knoten zum Herumlaufen; ab dann bewegst du diesen Knoten
nicht mehr selbst.
* `navAgentMoveTo` setzt ein Ziel und gibt `false` zurück, wenn es keinen Weg dorthin gibt. Darum
musst du dich kümmern, und es passiert oft.
* `onNavArrived` wird einmal aufgerufen, wenn ein Agent sein Ziel erreicht, und erfährt, welcher
Agent es ist.
* `navPath` beantwortet dieselbe Frage, ohne jemanden zu bewegen, und `nil` heißt: keine Route.
* Agenten steuern umeinander herum, nicht nur um das Level herum.
* Mit `navAgentGetVelocity` passt ein Laufzyklus zu der Strecke, die zurückgelegt wird.
[#_next_time_26]
=== Beim nächsten Mal
Deine beiden Kästen laufen jetzt schweigend durch einen Raum. In Lektion siebenundzwanzig geht es
darum, was du darunterlegst: Musik, die sauber in einer Schleife läuft und mit dem Video im Takt
bleibt, und MIDI, mit dem ein Spiel ein Instrument spielen kann -- oder ein echtes Instrument das
Spiel.