524 lines
23 KiB
Text
524 lines
23 KiB
Text
[#_lesson_24_physics]
|
|
== Lektion 24: Physik
|
|
|
|
image::learn/24-physics.png[Die fertige Lektion, 480]
|
|
|
|
Bis jetzt hat sich alles, was sich in deinen Spielen bewegt hat, bewegt, weil du gerechnet hast.
|
|
Eine Zahl ist in jedem Frame um zwei gestiegen; wenn sie über einen Rand hinaus war, hast du sie
|
|
umgedreht. Das ist der ehrliche Weg, ein Spiel in Bewegung zu bringen, und die meisten Spiele
|
|
bestehen daraus.
|
|
|
|
Diese Lektion gibt die Rechnerei an jemand anderen ab. Eine *Physik-Engine* simuliert die Welt --
|
|
Schwerkraft, Fallen, Rutschen, Stapeln, Abprallen, Rollen und Dinge, die zusammenstoßen -- und
|
|
bewegt deine Knoten für dich. Am Ende hast du eine Wanne voller Kisten und eine Kanone, die sie
|
|
umwirft. Im letzten Abschnitt geht es darum, wann du nichts davon tun solltest, und das ist der
|
|
Teil, den du zweimal lesen solltest.
|
|
|
|
[#_what_it_does_and_what_it_does_not]
|
|
=== Was sie tut und was nicht
|
|
|
|
Gib einem Knoten einen *Körper*, und die Simulation übernimmt ihn. Von da an fällt er durch die
|
|
Schwerkraft, landet auf dem, was unter ihm ist, kippt, wenn er außerhalb der Mitte getroffen
|
|
wird, liegt ruhig in einem Stapel, rutscht aus, bis er steht, und der Knoten folgt, wohin die
|
|
Simulation ihn auch setzt. Du setzt seine Position nicht mehr selbst. Das ist der Handel: Du
|
|
gibst Position und Drehung dieses Knotens ab und bekommst dafür Verhalten, das du nicht schreiben
|
|
musstest.
|
|
|
|
Was sie nicht tut, ist irgendetwas, das dein Spiel betrifft. Sie weiß nicht, was eine Punktzahl
|
|
ist, oder dass die rote Kiste die ist, die Punkte bringt, oder dass das Herunterfallen von der
|
|
Kante ein Leben kostet. Sie animiert keine gehende Figur, sie weiß nicht, wie sich ein Sprung
|
|
anfühlen soll, und sie wird ein Objekt nicht so bewegen, dass du es genau vorhersagen kannst.
|
|
Physik ist eine Maschine, die glaubwürdiges Chaos erzeugt.
|
|
|
|
Die Einheiten sind Meter, Kilogramm und Sekunden. Die Schwerkraft ist `0, -9.81, 0`, wenn du
|
|
nichts anderes sagst, und das ist die der Erde, nach unten, in Metern pro Sekunde zum Quadrat.
|
|
Eine Kiste, die eine Einheit breit ist, ist einen Meter breit, und sie fällt auch so. Wenn deine
|
|
Szene in einem anderen Maßstab gebaut ist, sieht alles aus, als fiele es durch Sirup oder durch
|
|
einen Zeichentrickfilm, und die Lösung ist, in Metern zu bauen.
|
|
|
|
[#_a_floor_and_one_crate]
|
|
=== Ein Boden und eine Kiste
|
|
|
|
Leg einen Ordner an, leg `crates.singe` an und tipp das ein. Es ist eine Szene, eine Platte, eine
|
|
Kiste über der Platte und drei Zeilen Physik.
|
|
|
|
[source,lua]
|
|
----
|
|
sceneEnable(true)
|
|
sceneSetBackground(16, 18, 26)
|
|
sceneSetAmbient(40, 42, 50)
|
|
|
|
local stone = materialNew()
|
|
materialSetColor(stone, 110, 110, 120)
|
|
materialSetRoughness(stone, 0.9)
|
|
|
|
local wood = materialNew()
|
|
materialSetColor(wood, 190, 140, 80)
|
|
materialSetRoughness(wood, 0.8)
|
|
|
|
local floor = nodeNew()
|
|
nodeSetMesh(floor, meshBox(16, 0.4, 16), stone)
|
|
nodeSetPosition(floor, 0, -0.2, 0)
|
|
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16)
|
|
|
|
local crate = nodeNew()
|
|
nodeSetMesh(crate, meshBox(0.8, 0.8, 0.8), wood)
|
|
nodeSetPosition(crate, 0.3, 4, 0)
|
|
nodeSetRotation(crate, 20, 0, 15)
|
|
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
|
|
bodySetMass(crate, 4)
|
|
|
|
local sun = lightNew(LIGHT_DIRECTIONAL)
|
|
nodeSetPosition(sun, -5, 9, 6)
|
|
nodeLookAt(sun, 0, 0, 0)
|
|
lightSetIntensity(sun, 1.7)
|
|
lightSetShadow(sun, true)
|
|
|
|
local camera = nodeNew()
|
|
nodeSetPosition(camera, 0, 4, 11)
|
|
nodeLookAt(camera, 0, 1, 0)
|
|
cameraSet(camera)
|
|
|
|
|
|
function onOverlayUpdate()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Starte es mit `Singe -R crates`. Die Kiste fällt aus vier Metern Höhe, landet auf einer Ecke,
|
|
weil du sie gekippt hast, schaukelt auf eine Seite und bleibt liegen. Drück *F5*, um es noch
|
|
einmal zu starten und es dir noch einmal anzusehen.
|
|
|
|
Nichts in `onOverlayUpdate` bewegt irgendetwas. Die beiden Aufrufe von `bodyNew` haben alles
|
|
erledigt.
|
|
|
|
[#_what_just_happened_20]
|
|
=== Was gerade passiert ist
|
|
|
|
[source,lua]
|
|
----
|
|
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16)
|
|
----
|
|
|
|
`bodyNew` gibt einem Knoten einen Körper. Es nimmt den Knoten, welche Art von Körper es ist,
|
|
welche Form die Simulation dafür benutzen soll, und die Größen dieser Form.
|
|
|
|
Es gibt drei Arten, und die richtige auszuwählen ist der größte Teil der Kunst.
|
|
|
|
* `BODY_STATIC` bewegt sich nie. Er wird dort hingesetzt, wo der Knoten ist, wenn du den Körper
|
|
erzeugst, und dort bleibt er für immer. Böden, Wände, der Erdboden, der Automat, das Level. Den
|
|
Knoten danach zu bewegen bewegt den Körper nicht, und das ist eine Falle, die man sich merken
|
|
sollte: statisch heißt statisch.
|
|
* `BODY_DYNAMIC` wird von der Simulation bewegt. Er fällt, prallt ab, rutscht, wird geschubst und
|
|
zieht seinen Knoten mit. Das ist eine Kiste, ein Fass, ein Ball, ein Trümmerstück.
|
|
* `BODY_KINEMATIC` geht dorthin, wo *du* seinen Knoten hinsetzt, und schiebt dynamische Körper aus
|
|
dem Weg, ohne je selbst geschubst zu werden. Eine bewegliche Plattform, ein Schläger, eine Tür
|
|
auf einer Schiene, die schwingende Faust einer Figur. Er ist die Brücke zwischen deiner
|
|
Rechnerei und der Simulation: Du steuerst ihn, er steuert sie.
|
|
|
|
Die Form ist nicht das Mesh. Sie ist ein grober Ersatz, den die Simulation benutzt, weil sie
|
|
Fragen über einen Quader viel schneller beantworten kann als über neunhundert Dreiecke.
|
|
`SHAPE_BOX` nimmt eine Breite, eine Höhe und eine Tiefe; `SHAPE_SPHERE` nimmt einen Radius;
|
|
`SHAPE_CAPSULE` und `SHAPE_CYLINDER` nehmen einen Radius und eine Höhe. Es gibt `SHAPE_HULL`, das
|
|
das Mesh unter dem Knoten wie mit Schrumpffolie umhüllt -- die übliche Antwort für ein Modell --
|
|
und `SHAPE_MESH`, die exakten Dreiecke, die statische Kulissen benutzen dürfen und ein fallendes
|
|
Objekt nicht.
|
|
|
|
Die Größen richtig hinzubekommen ist deine Sache. Nichts prüft, ob der Körper, den du beschrieben
|
|
hast, zu dem Mesh passt, das du gezeichnet hast, und eine Kiste, die mit `0.8` gezeichnet und
|
|
mit einem Körper von `2` erzeugt wurde, schwebt über dem Boden und wirft Dinge aus der Entfernung
|
|
um. Wenn sich etwas verhält, als hätte es eine andere Größe, dann hat es die auch.
|
|
|
|
[source,lua]
|
|
----
|
|
bodySetMass(crate, 4)
|
|
----
|
|
|
|
Die Masse eines dynamischen Körpers ergibt sich aus seiner Form bei der Dichte von Wasser, und
|
|
das macht Dinge viel schwerer, als du erwartest: Ein Quader von der Größe einer Tür wiegt
|
|
ungefähr hundertvierzig Kilogramm, und nichts, was du darauf werfen kannst, bewegt ihn. Setz die
|
|
Masse von allem, was sich leicht anfühlen soll. Vier Kilogramm sind eine Kiste, die du hochheben
|
|
könntest.
|
|
|
|
Die Masse ist wichtig, weil Kräfte durch sie geteilt werden. Derselbe Stoß bewegt ein leichtes
|
|
Ding weiter, und es braucht einen Impuls von Masse mal Geschwindigkeit, um etwas mit dieser
|
|
Geschwindigkeit in Bewegung zu setzen. Stimm zuerst die Masse ab und danach die Kräfte, sonst
|
|
drehst du dich im Kreis.
|
|
|
|
[#_making_things_move]
|
|
=== Dinge in Bewegung bringen
|
|
|
|
Drei Aufrufe schubsen einen dynamischen Körper herum, und sie sind nicht austauschbar.
|
|
|
|
`bodySetVelocity(node, x, y, z)` sagt „du bewegst dich jetzt mit dieser Geschwindigkeit“ und
|
|
ersetzt, was er vorher getan hat. Das ist ein Aufschlag, ein Abschuss, ein Erscheinen mit schon
|
|
etwas Tempo.
|
|
|
|
`bodyApplyImpulse(node, ix, iy, iz)` ist ein Treffer: ein Tritt, eine Explosion, ein Schläger, der
|
|
einen Ball trifft. Er kommt zu dem dazu, was der Körper schon getan hat. Gib ihm als drei weitere
|
|
Zahlen einen Punkt in der Welt, und er trifft *dort* statt in der Mitte, sodass eine Kiste, die
|
|
oben getroffen wird, umkippt, statt zu rutschen.
|
|
|
|
`bodyApplyForce(node, fx, fy, fz)` ist ein Schub, der anhält: eine Rakete, ein Ventilator, ein
|
|
Förderband. Eine Kraft, die einen Frame lang wirkt, tut fast nichts, und das überrascht Leute,
|
|
die einen Impuls erwartet haben.
|
|
|
|
Die Schwerkraft selbst ist `physicsSetGravity(x, y, z)`, und sie gilt für die ganze Welt. Der
|
|
Mond ist `physicsSetGravity(0, -1.62, 0)`, der Weltraum ist alles Nullen, und ein Spiel mit
|
|
seitlicher Schwerkraft ist eine Zeile entfernt.
|
|
|
|
Zwei weitere Einstellungen runden das Gefühl eines Objekts ab. `bodySetBounce(node, amount)`
|
|
reicht von `0`, das sofort liegen bleibt, bis `1`, das mit allem zurückkommt, womit es angekommen
|
|
ist; der Standard ist ein kaum spürbares `0.1`. `bodySetFriction(node, amount)` ist `0` für Eis
|
|
und standardmäßig `0.5`. Beide werden zwischen den zwei Dingen kombiniert, die sich berühren,
|
|
also braucht ein vereister Boden die kleine Zahl auf dem Boden, nicht auf allem, was darüber
|
|
läuft.
|
|
|
|
[#_knowing_when_things_touch]
|
|
=== Wissen, wann sich Dinge berühren
|
|
|
|
[source,lua]
|
|
----
|
|
function onCollision(nodeA, nodeB, x, y, z, speed)
|
|
if speed > 4 then
|
|
knocks = knocks + 1
|
|
end
|
|
end
|
|
----
|
|
|
|
`knocks` ist dort eine Variable von dir, die oben in der Datei mit `local knocks = 0` wartet, und
|
|
das Spielzeug weiter unten gibt sie aus.
|
|
|
|
`onCollision` ist ein Callback wie `onOverlayUpdate`: Du schreibst ihn, die Engine ruft ihn auf.
|
|
Er kommt einmal für jeden neuen Kontakt -- zwei Körper, die sich weiter berühren, melden sich
|
|
nicht noch einmal -- mit beiden Knoten, dem Punkt in der Welt, an dem sie sich getroffen haben,
|
|
und wie schnell sie sich getroffen haben. Durch die Geschwindigkeit können ein Anstupsen und ein
|
|
Krachen verschieden klingen, und sie ist der Grund, warum der Test oben das sanfte Setzen eines
|
|
Stapels ignoriert.
|
|
|
|
Die andere Hälfte des Paars ist `onTrigger`. Mach einen Körper mit `bodySetTrigger(node, true)`
|
|
zu einem Trigger, und er hört auf, Dinge zu schieben: Er wird zu einem Bereich, der über
|
|
`onTrigger(trigger, other, entered)` meldet, was hineinkommt und was ihn verlässt. Eine
|
|
Ziellinie, ein Türdurchgang, eine Grube, ein Gegenstand zum Aufsammeln. Er ist der 3D-Nachfahre
|
|
der Kollisionsprüfungen aus Lektion elf, und für alles, was nach „hat der Spieler diese Stelle
|
|
erreicht“ aussieht, ist er das, was du willst, statt eines festen Körpers.
|
|
|
|
[#_the_toy]
|
|
=== Das Spielzeug
|
|
|
|
Jetzt das Ganze. Fang mit dem Skript von oben an und bau es hierzu um: eine Wanne mit vier
|
|
Wänden, sechs Kisten in einer Pyramide und eine Stahlkugel, die du ausrichten und abschießen
|
|
kannst. Die Teile kennst du alle schon.
|
|
|
|
Alles, was sich der neue Code merkt, kommt ganz oben in die Datei, über die Szene:
|
|
|
|
[source,lua]
|
|
----
|
|
local crates = {}
|
|
local balls = {}
|
|
local aimX = 0
|
|
local aimLeft = false
|
|
local aimRight = false
|
|
local knocks = 0
|
|
local showShapes = false
|
|
----
|
|
|
|
Zu den Materialien kommen zwei weitere gemeinsam genutzte Handles: ein poliertes Metall für die
|
|
Kugel und die Zielmarkierung und ein Kisten-Mesh für alle sechs Kisten.
|
|
|
|
[source,lua]
|
|
----
|
|
local steel = materialNew()
|
|
materialSetColor(steel, 200, 205, 215)
|
|
materialSetMetallic(steel, 1)
|
|
materialSetRoughness(steel, 0.25)
|
|
|
|
local crateMesh = meshBox(0.8, 0.8, 0.8)
|
|
----
|
|
|
|
Die Wände der Wanne sind vier statische Quader, gemacht in einer Schleife aus einer Tabelle von
|
|
Platzierungen, und das ist Lektion sechs, die ehrliche Arbeit leistet:
|
|
|
|
[source,lua]
|
|
----
|
|
local wallMesh = meshBox(16, 1, 0.4)
|
|
|
|
for _, side in ipairs({ { 0, -8, 0 }, { 0, 8, 0 }, { -8, 0, 90 }, { 8, 0, 90 } }) do
|
|
local wall = nodeNew()
|
|
nodeSetMesh(wall, wallMesh, stone)
|
|
nodeSetPosition(wall, side[1], 0.5, side[2])
|
|
nodeSetRotation(wall, 0, side[3], 0)
|
|
bodyNew(wall, BODY_STATIC, SHAPE_BOX, 16, 1, 0.4)
|
|
end
|
|
----
|
|
|
|
Jeder Eintrag ist ein X, ein Z und eine Drehung um Y. Zwei Wände liegen quer hinten und vorn;
|
|
zwei sind um neunzig Grad gedreht und bilden die Seiten. Ein Mesh reicht für alle vier, und der
|
|
Körper wird erst erzeugt, nachdem der Knoten platziert und gedreht ist, weil ein statischer
|
|
Körper die Position und Drehung des Knotens aus dem Moment übernimmt, in dem er erzeugt wird.
|
|
|
|
Die Kisten kommen aus einer Funktion, damit sechs davon sechs Zeilen sind:
|
|
|
|
[source,lua]
|
|
----
|
|
local function crateAt(x, y)
|
|
local crate = nodeNew()
|
|
|
|
nodeSetMesh(crate, crateMesh, wood)
|
|
nodeSetPosition(crate, x, y, -2)
|
|
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
|
|
bodySetMass(crate, 4)
|
|
crates[#crates + 1] = crate
|
|
end
|
|
----
|
|
|
|
Sie bewahrt jede Kiste, die sie macht, in der Tabelle `crates` auf, und so kann der
|
|
Zeichen-Callback später allen eine Frage stellen. Sechs Aufrufe bauen die Pyramide, drei auf dem
|
|
Boden, zwei darauf und eine obendrauf:
|
|
|
|
[source,lua]
|
|
----
|
|
crateAt(-0.9, 0.4)
|
|
crateAt(0, 0.4)
|
|
crateAt(0.9, 0.4)
|
|
crateAt(-0.45, 1.25)
|
|
crateAt(0.45, 1.25)
|
|
crateAt(0, 2.1)
|
|
----
|
|
|
|
Die oberen Reihen sitzen eine Fingerbreite über denen darunter und fallen in dem Moment an ihren
|
|
Platz, in dem das Spiel startet. Eine kleine Lücke zu lassen ist einfacher, als genau zu stapeln,
|
|
und es ist freundlicher zur Simulation, als ihr einen Haufen zu geben, der schon zusammengepresst
|
|
ist.
|
|
|
|
Jeder Schuss macht eine neue Kugel:
|
|
|
|
[source,lua]
|
|
----
|
|
local function fire()
|
|
local ball = nodeNew()
|
|
|
|
nodeSetMesh(ball, meshSphere(0.35, 24), steel)
|
|
nodeSetPosition(ball, aimX, 1.2, 6)
|
|
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.35)
|
|
bodySetMass(ball, 3)
|
|
bodySetBounce(ball, 0.3)
|
|
bodySetVelocity(ball, 0, 2, -14)
|
|
balls[#balls + 1] = ball
|
|
if #balls > 5 then
|
|
nodeDelete(balls[1])
|
|
table.remove(balls, 1)
|
|
end
|
|
end
|
|
----
|
|
|
|
Sieh dir die Reihenfolge an. Der Knoten wird erzeugt, bekommt eine Form zum Zeichnen, wird
|
|
*platziert* und bekommt erst dann einen Körper. Ein dynamischer Körper fängt dort an, wo sein
|
|
Knoten ist, und danach läuft der Verkehr andersherum: Der Körper bewegt den Knoten. Wenn du die
|
|
Position eines dynamischen Knotens danach selbst setzt, kämpfst du gegen die Simulation, und die
|
|
Simulation gewinnt. Wenn etwas bewegt werden muss, gib ihm eine Geschwindigkeit, triff es mit
|
|
einem Impuls, oder mach es wie hier -- erzeug ein neues und lass das alte gehen.
|
|
|
|
Das alte gehen zu lassen ist `nodeDelete`, das den Knoten freigibt, alles darunter und den
|
|
Körper, der darauf mitfuhr. Ohne diese drei Zeilen bliebe jeder Schuss für immer in der Welt, und
|
|
ein paar hundert Kugeln später würde die Simulation eine Menge Arbeit für Dinge erledigen, die
|
|
niemand sehen kann. Aufräumen gehört dazu, wenn man Physik benutzt.
|
|
|
|
Die Steuerung und die Anzeige sind der Rest:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if what == SWITCH_BUTTON1 then
|
|
fire()
|
|
elseif what == SWITCH_BUTTON3 then
|
|
showShapes = not showShapes
|
|
if showShapes then
|
|
physicsSetDebug(DEBUG_SHAPES + DEBUG_CONTACTS)
|
|
else
|
|
physicsSetDebug(DEBUG_NONE)
|
|
end
|
|
elseif what == SWITCH_LEFT then
|
|
aimLeft = true
|
|
elseif what == SWITCH_RIGHT then
|
|
aimRight = true
|
|
end
|
|
end
|
|
----
|
|
|
|
Die Leertaste feuert, Umschalt schaltet die Physikansicht ein und aus, und mit gedrückten
|
|
Pfeiltasten verschiebst du den Abschusspunkt. Eine flache Metallplatte ohne Körper, die in jedem
|
|
Frame auf `aimX` gesetzt wird, zeigt, woher die nächste Kugel kommt. Das vollständige Skript, mit
|
|
dem Callback fürs Loslassen und dem Rest der Einrichtung, ist `24-physics.singe` im Ordner
|
|
`learn`.
|
|
|
|
`physicsSetDebug` ist der nützlichste Aufruf in dieser Lektion. Er zeichnet, was die Simulation
|
|
tatsächlich enthält -- die echten Formen als Drahtgitter und ein rotes Kreuz überall dort, wo
|
|
sich in diesem Schritt zwei Körper berührt haben -- über deine Szene. Wenn zum ersten Mal eine
|
|
Kiste eine Handbreit über dem Boden schwebt, durch ihn hindurchfällt oder etwas aus zwei Metern
|
|
Entfernung umwirft, schalte das ein, und die Antwort steht auf dem Bildschirm. Füg
|
|
`DEBUG_STATIC` hinzu, um auch den Boden und die Wände zu sehen. Solange es aus ist, kostet es
|
|
nichts.
|
|
|
|
Zum Schluss das Zählen:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
local over = 0
|
|
local settled = true
|
|
|
|
if aimLeft then
|
|
aimX = math.max(aimX - 0.08, -5)
|
|
end
|
|
if aimRight then
|
|
aimX = math.min(aimX + 0.08, 5)
|
|
end
|
|
nodeSetPosition(marker, aimX, 0.05, 6)
|
|
|
|
for _, crate in ipairs(crates) do
|
|
local pitch, _, roll = nodeGetRotation(crate)
|
|
|
|
if math.abs(pitch) > 30 or math.abs(roll) > 30 then
|
|
over = over + 1
|
|
end
|
|
if not bodyIsResting(crate) then
|
|
settled = false
|
|
end
|
|
end
|
|
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Arrows aim. Space fires. Shift shows the shapes.")
|
|
overlayPrint(2, 4, "knocked over " .. over .. " of " .. #crates .. " hard hits " .. knocks)
|
|
if settled then
|
|
overlayPrint(2, 6, "everything has settled")
|
|
end
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Eine Kiste zählt als umgeworfen, wenn sie mehr als dreißig Grad geneigt ist, und das ist eine
|
|
Spielregel, geschrieben in einer Zeile Rechnerei, die auf dem aufsetzt, was die Simulation getan
|
|
hat. `nodeGetRotation` liefert drei Winkel, und hier werden der erste und der dritte gebraucht,
|
|
also wird der mittlere in einer Variablen namens `_` aufgefangen. Dieser Unterstrich ist ein
|
|
gewöhnlicher Name, an dem nichts Besonderes ist, und wird nach Konvention benutzt, um zu sagen:
|
|
„Den hier ignoriere ich.“
|
|
|
|
`bodyIsResting` sagt, ob die Simulation einen Körper schlafen gelegt hat, weil er sich nicht mehr
|
|
bewegt. Daran erkennst du, dass ein Schuss vorbei ist: kein Timer, kein Raten, sondern jede Kiste
|
|
schläft. Ein Bowlingspiel wertet seinen Frame genau an dieser Stelle.
|
|
|
|
[#_when_not_to_use_it]
|
|
=== Wann du sie nicht benutzen solltest
|
|
|
|
Physik ist das verlockendste Werkzeug in jeder Engine und das, das die meisten Wochenenden
|
|
verschwendet. Drei Dinge sind über sie wahr, und keins davon ist die Schuld der Engine.
|
|
|
|
*Sie ist teuer.* Jeder dynamische Körper ist Arbeit in jedem Schritt, sechzigmal pro Sekunde, ob
|
|
ihn jemand ansieht oder nicht. Vierzig Kisten sind in Ordnung. Viertausend nicht, und auf einem
|
|
kleinen Rechner auch vierhundert nicht.
|
|
|
|
*Sie ist unberechenbar.* Starte dein Spielzeug zweimal, und die Kisten landen anders. Das lässt es
|
|
lebendig aussehen, und es ist auch der Grund, warum sich ein Spiel bei nichts darauf verlassen
|
|
kann, was jedes Mal gleich ausgehen muss. Eine Tür, die aufgehen muss, eine Plattform, die
|
|
ankommen muss, ein Sprung, den der Spieler schaffen können muss: Das ist Rechnerei, oder ein
|
|
Tween, oder ein kinematischer Körper auf einem Pfad, den du geschrieben hast. Überlass nie etwas,
|
|
das der Spieler tun muss, einer Simulation, die es hinter eine Kiste fallen lassen kann.
|
|
|
|
*Sie ist schwer abzustimmen.* Die Masse, die Reibung, das Abprallen, die Form und die Größe ziehen
|
|
alle aneinander, und „es fühlt sich falsch an“ hat selten nur eine Ursache. Jede Stunde, die du
|
|
damit verbringst, dass sich eine Physik-Kiste gut anfühlt, ist eine Stunde, die du nicht mit dem
|
|
Spiel verbracht hast.
|
|
|
|
Greif also zu ihr, wenn das Chaos der eigentliche Zweck *ist*: Dinge, die umkippen, fliegende
|
|
Trümmer, ein Haufen, der sich setzt, ein Ball, der durch einen Raum scheppert, ein Fahrzeug, das
|
|
über unebenes Gelände rollt, eine Ragdoll, die eine Treppe hinunterpurzelt. Das kann niemand von
|
|
Hand schreiben, und niemand kann dir genau sagen, wie es aussehen soll.
|
|
|
|
Greif nicht zu ihr, wenn du weißt, was passieren soll. Ein Geschoss, das geradeaus fliegt, sind
|
|
zwei Zeilen aus Lektion zwei. Eine Plattform, die hoch- und runterfährt, ist `math.sin`. Eine
|
|
Spielfigur, die so geht und springt, wie Spieler es erwarten, will einen Character-Controller
|
|
(`playerNew`, in der Referenz), weil ein Mensch kein Quader ist und sich nie wie einer verhält.
|
|
Ein Gegner, der patrouilliert, will einen Pfad. Ein Menü, das hereingleitet, will einen Tween.
|
|
|
|
Die Regel, die am besten taugt: Benutz Physik für die Dinge, denen der Spieler zusieht, und
|
|
Rechnerei für die Dinge, die der Spieler tut.
|
|
|
|
[#_try_it_24]
|
|
=== Probier es aus
|
|
|
|
. *Ändere die Welt.* Füg `physicsSetGravity(0, -1.62, 0)` weit oben ein und feuere noch einmal.
|
|
Dann probier `0, -30, 0`. Dieselben Kisten, ein anderer Planet, eine Zeile.
|
|
. *Ändere die Kugel.* Mach die Kugel mit `bodySetMass(ball, 30)` zehnmal schwerer, dann mit
|
|
`0.3` zehnmal leichter, und lass die Geschwindigkeit gleich. Dann setz die Masse zurück und
|
|
ändere stattdessen die `-14`. Zwei Wege, härter zuzuschlagen, und sie fühlen sich nicht gleich
|
|
an.
|
|
. *Mach die Kisten zu Eis.* Füg in `crateAt` `bodySetFriction(crate, 0.02)` ein und füg eine
|
|
Zeile hinzu, die auch dem Boden eine niedrige Reibung gibt. Sieh zu, wie sich der Haufen von
|
|
vornherein weigert, stehen zu bleiben.
|
|
. *Mach die Form absichtlich falsch.* Ändere in `crateAt` die Größen des Körpers auf `2, 2, 2`,
|
|
während das Mesh bei `0.8` bleibt. Starte es, sieh zu, wie die Kisten schweben und sich aus dem
|
|
Nichts schubsen, dann drück Umschalt und sieh zu, wie die Debug-Ansicht es in einem Bild
|
|
erklärt.
|
|
. *Werte es richtig.* Benutz `bodyIsResting` auf jeder Kiste, um zu bemerken, wann der Schuss
|
|
vorbei ist, und zähl erst dann, wie viele umgefallen sind, und addier das zu einer Punktzahl.
|
|
Das ist die Wertungshälfte eines Bowlingspiels.
|
|
|
|
[#_break_it_on_purpose_23]
|
|
=== Mach es absichtlich kaputt
|
|
|
|
Füg unter dem `bodyNew` des Bodens eine Zeile ein, die den Boden schwer macht, und das ist etwas,
|
|
von dem man vernünftigerweise denken könnte, dass man es will:
|
|
|
|
[source,lua]
|
|
----
|
|
bodySetMass(floor, 200)
|
|
----
|
|
|
|
Singe hält das Spiel mit etwas wie dem hier an:
|
|
|
|
----
|
|
32:bodySetMass: Node 1 is not a dynamic body, or the mass is not positive.
|
|
----
|
|
|
|
Die erste Zahl ist die Zeile in deiner eigenen Datei, also wird deine anders sein.
|
|
|
|
Die Zahl nach `Node` ist das Handle des Knotens, die Zahl, die `nodeNew` zurückgegeben hat. Die
|
|
Beschwerde ist genau: Masse gehört zu Dingen, die die Simulation bewegt, und ein statischer
|
|
Körper wird von nichts bewegt, also hat er keine Masse, die man setzen könnte. Dass du danach
|
|
fragst, ist ein Zeichen, dass der Körper die falsche Art für das ist, was du im Sinn hattest --
|
|
und wenn du wirklich einen schweren Boden willst, der sich verschieben lässt, dann willst du einen
|
|
dynamischen Körper mit großer Masse oder einen kinematischen, den du selbst bewegst.
|
|
|
|
Dieselbe Meldung erscheint bei einer Masse von `0`, die Leute schreiben, wenn sie „schwerelos“
|
|
meinen. Schwerelos ist keine Masse. Es ist eine Welt ohne Schwerkraft oder
|
|
`playerSetGravityScale` auf einem Character-Controller.
|
|
|
|
[#_what_you_learned_24]
|
|
=== Was du gelernt hast
|
|
|
|
* Ein Körper hängt an einem Knoten, und von da an sind Knoten und Körper ein Ding.
|
|
* Statisch bewegt sich nie, dynamisch wird von der Simulation bewegt, und kinematisch wird von
|
|
dir bewegt und schiebt alles andere beiseite.
|
|
* Die Kollisionsform ist ein Ersatz für das Mesh, und du bist dafür verantwortlich, dass ihre
|
|
Größen zu dem passen, was du gezeichnet hast.
|
|
* Die Masse ist standardmäßig das Volumen der Form bei der Dichte von Wasser, also setz sie bei
|
|
allem, was sich leicht anfühlen soll.
|
|
* `bodySetVelocity` ersetzt die Bewegung eines Körpers, `bodyApplyImpulse` ist ein Treffer, und
|
|
`bodyApplyForce` ist ein Schub, der anhält.
|
|
* Setz nicht die Position eines dynamischen Knotens; lösch ihn und mach einen neuen, wenn du ihn
|
|
neu anfangen lassen musst.
|
|
* `onCollision` meldet jeden neuen Kontakt mit seinem Punkt und seiner Geschwindigkeit, und ein
|
|
Trigger-Körper meldet Ankünfte und Abgänge über `onTrigger`.
|
|
* `bodyIsResting` sagt dir, wann alles stillsteht, und dann ist meistens ein Zug vorbei.
|
|
* `physicsSetDebug` zeichnet die eigene Sicht der Simulation, und es beantwortet die meisten
|
|
Physikfragen schneller, als Code zu lesen.
|
|
* Physik ist teuer, nicht wiederholbar und fummelig. Benutz sie für das Chaos, nicht für die
|
|
Dinge, von denen das Spiel abhängt.
|
|
|
|
[#_next_time_24]
|
|
=== Beim nächsten Mal
|
|
|
|
Fallende Kisten sind eine Art von Spektakel. Die andere Art besteht aus tausend winzigen Dingen,
|
|
die eine halbe Sekunde leben: Funken, Rauch, Feuer, Regen und Staub. In der nächsten Lektion geht
|
|
es um Emitter, und sie endet mit einer Explosion. Das ist Lektion fünfundzwanzig.
|