588 lines
26 KiB
Text
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.
|