Grundlegende 2D-Platformer-Physik, Teil 1
() translation by (you can also view the original English article)
Zeichenkollisionen
Okay, die Prämisse sieht also so aus: Wir wollen einen 2D-Plattformer mit einfacher, robuster, reaktionsschneller, genauer und vorhersehbarer Physik erstellen. In diesem Fall möchten wir keine große 2D-Physik-Engine verwenden, und dafür gibt es einige Gründe:
- unvorhersehbare Kollisionsreaktionen
- Es ist schwierig, eine genaue und robuste Charakterbewegung einzurichten
- viel komplizierter zu arbeiten
- benötigt viel mehr Rechenleistung als einfache Physik
Natürlich gibt es auch viele Profis, die eine handelsübliche Physik-Engine verwenden, beispielsweise die Möglichkeit, komplexe physikalische Interaktionen ganz einfach einzurichten, aber das ist nicht das, was wir für unser Spiel benötigen.
Eine benutzerdefinierte Physik-Engine verleiht dem Spiel ein benutzerdefiniertes Gefühl, und das ist wirklich wichtig! Selbst wenn Sie mit einem relativ einfachen Setup beginnen, wird die Art und Weise, wie sich Dinge bewegen und miteinander interagieren, immer nur von Ihren eigenen Regeln beeinflusst und nicht von denen anderer. Lasst uns anfangen!
Zeichengrenzen
Beginnen wir damit, zu definieren, welche Formen wir in unserer Physik verwenden werden. Eine der grundlegendsten Formen, mit denen wir ein physisches Objekt in einem Spiel darstellen können, ist eine Axis Aligned Bounding Box (AABB). AABB ist im Grunde ein nicht gedrehtes Rechteck.



In vielen Plattformspielen reichen AABBs aus, um den Körper jedes Objekts im Spiel zu approximieren. Sie sind äußerst effektiv, da es sehr einfach ist, eine Überlappung zwischen AABBs zu berechnen, und nur sehr wenige Daten erforderlich sind. Um einen AABB zu beschreiben, reicht es aus, dessen Zentrum und Größe zu kennen.
Lassen Sie uns ohne weiteres eine Struktur für unseren AABB erstellen.
1 |
public struct AABB |
2 |
{
|
3 |
}
|
Wie bereits erwähnt, benötigen wir hier nur zwei Vektoren, um Daten zu erhalten. Das erste ist das Zentrum des AABB und das zweite die halbe Größe. Warum halbe Größe? Die meiste Zeit für Berechnungen benötigen wir sowieso die halbe Größe. Anstatt sie jedes Mal zu berechnen, merken wir sie uns einfach anstelle der vollen Größe.
1 |
public struct AABB |
2 |
{
|
3 |
public Vector2 center; |
4 |
public Vector2 halfSize; |
5 |
}
|
Beginnen wir mit dem Hinzufügen eines Konstruktors, damit die Struktur mit benutzerdefinierten Parametern erstellt werden kann.
1 |
public AABB(Vector2 center, Vector2 halfSize) |
2 |
{
|
3 |
this.center = center; |
4 |
this.halfSize = halfSize; |
5 |
}
|
Damit können wir die Kollisionsprüfungsfunktionen erstellen. Lassen Sie uns zunächst einfach überprüfen, ob zwei AABBs miteinander kollidieren. Dies ist sehr einfach - wir müssen nur sehen, ob der Abstand zwischen den Mittelpunkten auf jeder Achse kleiner ist als die Summe der halben Größen.
1 |
public bool Overlaps(AABB other) |
2 |
{
|
3 |
if ( Mathf.Abs(center.x - other.center.x) > halfSize.x + other.halfSize.x ) return false; |
4 |
if ( Mathf.Abs(center.y - other.center.y) > halfSize.y + other.halfSize.y ) return false; |
5 |
return true; |
6 |
}
|
Hier ist ein Bild, das diese Prüfung auf der x-Achse zeigt. Die y-Achse wird auf die gleiche Weise überprüft.



Wie Sie sehen können, wäre keine Überlappung möglich, wenn die Summe der halben Größen kleiner als der Abstand zwischen den Zentren wäre. Beachten Sie, dass wir im obigen Code der Kollisionsprüfung frühzeitig entkommen können, wenn wir feststellen, dass sich die Objekte auf der ersten Achse nicht überlappen. Die Überlappung muss auf beiden Achsen vorhanden sein, wenn die AABBs im 2D-Raum kollidieren sollen.
Objekt verschieben
Beginnen wir mit der Erstellung einer Klasse für ein Objekt, das von der Physik des Spiels beeinflusst wird. Später werden wir dies als Basis für ein tatsächliches Spielerobjekt verwenden. Nennen wir diese Klasse MovingObject.
1 |
public class MovingObject |
2 |
{
|
3 |
}
|
Füllen wir nun diese Klasse mit den Daten. Wir werden ziemlich viele Informationen für dieses Objekt benötigen:
- Position und Position des vorherigen Frames
- Geschwindigkeit und Geschwindigkeit des vorherigen Frames
- Rahmen
- AABB und ein Offset dafür (damit wir es mit einem Sprite ausrichten können)
- ist Objekt auf dem Boden und ob es auf dem Boden letzten Rahmen war
- ist ein Objekt neben der Wand auf der linken Seite und ob es neben dem letzten Frame war
- ist ein Objekt neben der Wand auf der rechten Seite und ob es neben dem letzten Frame war
- ist Objekt an der Decke und ob es an der Decke letzten Rahmen war
Position, Geschwindigkeit und Skalierung sind 2D-Vektoren.
1 |
public class MovingObject |
2 |
{
|
3 |
public Vector2 mOldPosition; |
4 |
public Vector2 mPosition; |
5 |
|
6 |
public Vector2 mOldSpeed; |
7 |
public Vector2 mSpeed; |
8 |
|
9 |
public Vector2 mScale; |
10 |
}
|
Fügen wir nun den AABB und den Offset hinzu. Der Offset wird benötigt, damit wir den AABB frei an das Sprite des Objekts anpassen können.
1 |
public AABB mAABB; |
2 |
public Vector2 mAABBOffset; |
Zum Schluss deklarieren wir die Variablen, die den Positionszustand des Objekts angeben, egal ob es sich auf dem Boden, neben einer Wand oder an der Decke befindet. Diese sind sehr wichtig, weil sie uns wissen lassen, ob wir springen können oder zum Beispiel einen Sound spielen müssen, nachdem wir gegen eine Wand gestoßen sind.
1 |
public bool mPushedRightWall; |
2 |
public bool mPushesRightWall; |
3 |
|
4 |
public bool mPushedLeftWall; |
5 |
public bool mPushesLeftWall; |
6 |
|
7 |
public bool mWasOnGround; |
8 |
public bool mOnGround; |
9 |
|
10 |
public bool mWasAtCeiling; |
11 |
public bool mAtCeiling; |
Das sind die Grundlagen. Jetzt erstellen wir eine Funktion, die das Objekt aktualisiert. Im Moment werden wir nicht alles einrichten, aber gerade genug, um grundlegende Zeichensteuerelemente zu erstellen.
1 |
public void UpdatePhysics() |
2 |
{
|
3 |
}
|
Das erste, was wir hier tun möchten, ist, die Daten des vorherigen Frames in den entsprechenden Variablen zu speichern.
1 |
public void UpdatePhysics() |
2 |
{
|
3 |
mOldPosition = mPosition; |
4 |
mOldSpeed = mSpeed; |
5 |
|
6 |
mWasOnGround = mOnGround; |
7 |
mPushedRightWall = mPushesRightWall; |
8 |
mPushedLeftWall = mPushesLeftWall; |
9 |
mWasAtCeiling = mAtCeiling; |
10 |
}
|
Jetzt aktualisieren wir die Position mit der aktuellen Geschwindigkeit.
1 |
mPosition += mSpeed*Time.deltaTime; |
Und jetzt machen wir es so, dass wir annehmen, dass der Charakter auf dem Boden liegt, wenn die vertikale Position kleiner als Null ist. Das ist nur für den Moment, damit wir die Steuerelemente des Charakters einrichten können. Später werden wir eine Kollision mit einer Tilemap durchführen.
1 |
if (mPosition.y < 0.0f) |
2 |
{
|
3 |
mPosition.y = 0.0f; |
4 |
mOnGround = true; |
5 |
}
|
6 |
else
|
7 |
mOnGround = false; |
Danach müssen wir auch das AABB-Zentrum aktualisieren, damit es tatsächlich der neuen Position entspricht.
1 |
mAABB.center = mPosition + mAABBOffset; |
Für das Demo-Projekt verwende ich Unity. Um die Position des Objekts zu aktualisieren, muss es auf die Transformationskomponente angewendet werden. Lassen Sie uns dies auch tun. Gleiches muss für die Waage getan werden.
1 |
mTransform.position = new Vector3(Mathf.Round(mPosition.x), Mathf.Round(mPosition.y),-1.0f); |
2 |
mTransform.localScale = new Vector3(mScale.x, mScale.y, 1.0f); |
Wie Sie sehen können, wird die gerenderte Position aufgerundet. Dies soll sicherstellen, dass das gerenderte Zeichen immer an einem Pixel ausgerichtet ist.
Zeichensteuerung
Daten
Nachdem wir unsere grundlegende MovingObject-Klasse fertiggestellt haben, können wir zunächst mit der Charakterbewegung spielen. Es ist schließlich ein sehr wichtiger Teil des Spiels und kann ziemlich sofort erledigt werden - es ist noch nicht nötig, zu tief in die Spielsysteme einzutauchen, und es wird fertig sein, wenn wir unseren Charakter testen müssen - Kartenkollisionen.
Lassen Sie uns zunächst eine Zeichenklasse erstellen und von der MovingObject-Klasse ableiten.
1 |
public class Character : MovingObject |
2 |
{
|
3 |
}
|
Wir müssen hier ein paar Dinge erledigen. Zunächst die Eingaben - lassen Sie uns eine Aufzählung erstellen, die alle Steuerelemente für den Charakter abdeckt. Erstellen wir es in einer anderen Datei und nennen es KeyInput.
1 |
public enum KeyInput |
2 |
{
|
3 |
GoLeft = 0, |
4 |
GoRight, |
5 |
GoDown, |
6 |
Jump, |
7 |
Count
|
8 |
}
|
Wie Sie sehen können, kann sich unser Charakter nach links, rechts, unten und oben bewegen. Das Abwärtsbewegen funktioniert nur auf Einwegplattformen, wenn wir durch sie fallen wollen.
Deklarieren wir nun zwei Arrays in der Zeichenklasse, eines für die Eingaben des aktuellen Frames und eines für die Eingaben des vorherigen Frames. Je nach Spiel kann dieses Setup mehr oder weniger sinnvoll sein. Anstatt den Schlüsselstatus in einem Array zu speichern, wird er normalerweise bei Bedarf mithilfe der spezifischen Funktionen einer Engine oder eines Frameworks überprüft. Ein Array, das nicht streng an reale Eingaben gebunden ist, kann jedoch von Vorteil sein, wenn wir beispielsweise Tastendrücke simulieren möchten.
1 |
protected bool[] mInputs; |
2 |
protected bool[] mPrevInputs; |
Diese Arrays werden von der KeyInput-Enumeration indiziert. Um diese Arrays einfach zu verwenden, erstellen wir einige Funktionen, mit denen wir nach einem bestimmten Schlüssel suchen können.
1 |
protected bool Released(KeyInput key) |
2 |
{
|
3 |
return (!mInputs[(int)key] && mPrevInputs[(int)key]); |
4 |
}
|
5 |
|
6 |
protected bool KeyState(KeyInput key) |
7 |
{
|
8 |
return (mInputs[(int)key]); |
9 |
}
|
10 |
|
11 |
protected bool Pressed(KeyInput key) |
12 |
{
|
13 |
return (mInputs[(int)key] && !mPrevInputs[(int)key]); |
14 |
}
|
Hier gibt es nichts Besonderes - wir möchten sehen können, ob eine Taste gerade gedrückt, gerade losgelassen oder ein- oder ausgeschaltet wurde.
Jetzt erstellen wir eine weitere Aufzählung, die alle möglichen Zustände des Charakters enthält.
1 |
public enum CharacterState |
2 |
{
|
3 |
Stand, |
4 |
Walk, |
5 |
Jump, |
6 |
GrabLedge, |
7 |
};
|
Wie Sie sehen können, kann unser Charakter entweder stehen bleiben, gehen, springen oder einen Vorsprung ergreifen. Nachdem dies erledigt ist, müssen wir Variablen wie Sprunggeschwindigkeit, Gehgeschwindigkeit und aktuellen Status hinzufügen.
1 |
public CharacterState mCurrentState = CharacterState.Stand; |
2 |
public float mJumpSpeed; |
3 |
public float mWalkSpeed; |
Natürlich werden hier einige weitere Daten benötigt, z. B. das Sprite für Charaktere, aber wie dies aussehen wird, hängt stark davon ab, welche Art von Engine Sie verwenden werden. Da ich Unity verwende, verwende ich einen Verweis auf einen Animator, um sicherzustellen, dass das Sprite die Animation für einen geeigneten Status wiedergibt.
Loop aktualisieren
Okay, jetzt können wir mit der Arbeit an der Update-Schleife beginnen. Was wir hier machen, hängt vom aktuellen Status des Charakters ab.
1 |
public void CharacterUpdate() |
2 |
{
|
3 |
switch (mCurrentState) |
4 |
{
|
5 |
case CharacterState.Stand: |
6 |
break; |
7 |
case CharacterState.Walk: |
8 |
break; |
9 |
case CharacterState.Jump: |
10 |
break; |
11 |
case CharacterState.GrabLedge: |
12 |
break; |
13 |
}
|
14 |
}
|
Stand State
Beginnen wir damit, zu füllen, was zu tun ist, wenn sich der Charakter nicht bewegt - im Standzustand. Zunächst sollte die Geschwindigkeit auf Null gesetzt werden.
1 |
case CharacterState.Stand: |
2 |
mSpeed = Vector2.zero; |
3 |
break; |
Wir wollen auch das passende Sprite für den Staat zeigen.
1 |
case CharacterState.Stand: |
2 |
mSpeed = Vector2.zero; |
3 |
mAnimator.Play("Stand"); |
4 |
break; |
Wenn der Charakter nicht auf dem Boden liegt, kann er nicht mehr stehen, sodass wir den Status ändern müssen, um zu springen.
1 |
case CharacterState.Stand: |
2 |
mSpeed = Vector2.zero; |
3 |
mAnimator.Play("Stand"); |
4 |
|
5 |
if (!mOnGround) |
6 |
{
|
7 |
mCurrentState = CharacterState.Jump; |
8 |
break; |
9 |
}
|
10 |
break; |
Wenn die GoLeft- oder GoRight-Taste gedrückt wird, müssen wir unseren Status ändern, um gehen zu können.
1 |
case CharacterState.Stand: |
2 |
mSpeed = Vector2.zero; |
3 |
mAnimator.Play("Stand"); |
4 |
|
5 |
if (!mOnGround) |
6 |
{
|
7 |
mCurrentState = CharacterState.Jump; |
8 |
break; |
9 |
}
|
10 |
|
11 |
if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft)) |
12 |
{
|
13 |
mCurrentState = CharacterState.Walk; |
14 |
break
|
15 |
}
|
16 |
break; |
Wenn die Sprungtaste gedrückt wird, möchten wir die vertikale Geschwindigkeit auf die Sprunggeschwindigkeit einstellen und den Zustand zum Springen ändern.
1 |
if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft)) |
2 |
{
|
3 |
mCurrentState = CharacterState.Walk; |
4 |
break; |
5 |
}
|
6 |
else if (KeyState(KeyInput.Jump)) |
7 |
{
|
8 |
mSpeed.y = mJumpSpeed; |
9 |
mCurrentState = CharacterState.Jump; |
10 |
break; |
11 |
}
|
Das wird es zumindest vorerst für diesen Zustand sein.
Walk State
Erstellen wir nun eine Logik für die Bewegung auf dem Boden und beginnen sofort mit dem Abspielen der Laufanimation.
1 |
case CharacterState.Walk: |
2 |
mAnimator.Play("Walk"); |
3 |
break; |
Wenn wir hier nicht die linke oder rechte Taste drücken oder beide gedrückt werden, möchten wir in den Stillstand zurückkehren.
1 |
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft)) |
2 |
{
|
3 |
mCurrentState = CharacterState.Stand; |
4 |
mSpeed = Vector2.zero; |
5 |
break; |
6 |
}
|
Wenn die GoRight-Taste gedrückt wird, müssen wir die horizontale Geschwindigkeit auf mWalkSpeed einstellen und sicherstellen, dass das Sprite entsprechend skaliert ist. Die horizontale Skalierung muss geändert werden, wenn das Sprite horizontal gedreht werden soll.
Wir sollten uns auch nur bewegen, wenn tatsächlich kein Hindernis vor uns liegt. Wenn also mPushesRightWall auf true gesetzt ist, sollte die horizontale Geschwindigkeit auf Null gesetzt werden, wenn wir uns nach rechts bewegen.
1 |
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft)) |
2 |
{
|
3 |
mCurrentState = CharacterState.Stand; |
4 |
mSpeed = Vector2.zero; |
5 |
break; |
6 |
}
|
7 |
else if (KeyState(KeyInput.GoRight)) |
8 |
{
|
9 |
if (mPushesRightWall) |
10 |
mSpeed.x = 0.0f; |
11 |
else
|
12 |
mSpeed.x = mWalkSpeed; |
13 |
|
14 |
mScale.x = Mathf.Abs(mScale.x); |
15 |
}
|
16 |
else if (KeyState(KeyInput.GoLeft)) |
17 |
{
|
18 |
if (mPushesLeftWall) |
19 |
mSpeed.x = 0.0f; |
20 |
else
|
21 |
mSpeed.x = -mWalkSpeed; |
22 |
|
23 |
mScale.x = -Mathf.Abs(mScale.x); |
24 |
}
|
Wir müssen auch die linke Seite auf die gleiche Weise behandeln.
Wie im Stand müssen wir sehen, ob eine Sprungtaste gedrückt wird, und die vertikale Geschwindigkeit einstellen, wenn dies der Fall ist.
1 |
if (KeyState(KeyInput.Jump)) |
2 |
{
|
3 |
mSpeed.y = mJumpSpeed; |
4 |
mAudioSource.PlayOneShot(mJumpSfx, 1.0f); |
5 |
mCurrentState = CharacterState.Jump; |
6 |
break; |
7 |
}
|
Wenn sich der Charakter nicht auf dem Boden befindet, muss er den Status ändern, um ebenfalls zu springen, jedoch ohne zusätzliche vertikale Geschwindigkeit, sodass er einfach herunterfällt.
1 |
if (KeyState(KeyInput.Jump)) |
2 |
{
|
3 |
mSpeed.y = mJumpSpeed; |
4 |
mAudioSource.PlayOneShot(mJumpSfx, 1.0f); |
5 |
mCurrentState = CharacterState.Jump; |
6 |
break; |
7 |
}
|
8 |
else if (!mOnGround) |
9 |
{
|
10 |
mCurrentState = CharacterState.Jump; |
11 |
break; |
12 |
}
|
Das war's für das Gehen. Gehen wir zum Sprungzustand über.
Sprungzustand
Beginnen wir mit der Einstellung einer geeigneten Animation für das Sprite.
1 |
mAnimator.Play("Jump"); |
Im Sprungzustand müssen wir die Geschwindigkeit des Charakters durch Schwerkraft erhöhen, damit er immer schneller in Richtung Boden geht.
1 |
mSpeed.y += Constants.cGravity * Time.deltaTime; |
Es wäre jedoch sinnvoll, ein Limit hinzuzufügen, damit der Charakter nicht zu schnell fallen kann.
1 |
mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed); |
In vielen Spielen, wenn der Charakter in der Luft ist, nimmt die Manövrierfähigkeit ab, aber wir werden einige sehr einfache und genaue Steuerungen wählen, die volle Flexibilität in der Luft ermöglichen. Wenn wir also die GoLeft- oder GoRight-Taste drücken, bewegt sich der Charakter in die Richtung, während er so schnell springt, als wäre er am Boden. In diesem Fall können wir einfach die Bewegungslogik aus dem Gehzustand kopieren.
1 |
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft)) |
2 |
{ |
3 |
mSpeed.x = 0.0f; |
4 |
} |
5 |
else if (KeyState(KeyInput.GoRight)) |
6 |
{ |
7 |
if (mPushesRightWall) |
8 |
mSpeed.x = 0.0f; |
9 |
else |
10 |
mSpeed.x = mWalkSpeed; |
11 |
mScale.x = Mathf.Abs(mScale.x); |
12 |
} |
13 |
else if (KeyState(KeyInput.GoLeft)) |
14 |
{ |
15 |
if (mPushesLeftWall) |
16 |
mSpeed.x = 0.0f; |
17 |
else |
18 |
mSpeed.x = -mWalkSpeed; |
19 |
mScale.x = -Mathf.Abs(mScale.x); |
20 |
} |
Schließlich werden wir den Sprung höher machen, wenn die Sprungtaste länger gedrückt wird. Um das zu tun, machen wir den Sprung tatsächlich niedriger, wenn die Sprungtaste nicht gedrückt wird.
1 |
if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f) |
2 |
mSpeed.y = Mathf.Min(mSpeed.y, Constants.cMinJumpSpeed); |
Wie Sie sehen können, wird die Geschwindigkeit auf den Maximalwert von cMinJumpSpeed
(200 Pixel pro Sekunde) begrenzt, wenn die Sprungtaste nicht gedrückt wird und die vertikale Geschwindigkeit positiv ist. Das bedeutet, dass, wenn wir nur auf die Sprungtaste tippen, die Sprunggeschwindigkeit nicht gleich mJumpSpeed
(standardmäßig 410) ist, sondern auf 200 gesenkt wird und der Sprung daher kürzer ist.
Da wir noch keine Ebenengeometrie haben, sollten wir die GrabLedge-Implementierung vorerst überspringen.
Aktualisieren Sie die vorherigen Eingaben
Sobald der Frame fertig ist, können wir die vorherigen Eingaben aktualisieren. Erstellen wir hierfür eine neue Funktion. Hier müssen Sie lediglich die Schlüsselstatuswerte aus dem Array mInputs
in das Array mPrevInputs
verschieben.
1 |
public void UpdatePrevInputs() |
2 |
{
|
3 |
var count = (byte)KeyInput.Count; |
4 |
|
5 |
for (byte i = 0; i < count; ++i) |
6 |
mPrevInputs[i] = mInputs[i]; |
7 |
}
|
Ganz am Ende der CharacterUpdate-Funktion müssen wir noch einige Dinge tun. Das erste ist, die Physik zu aktualisieren.
1 |
UpdatePhysics(); |
Nachdem die Physik aktualisiert wurde, können wir sehen, ob wir einen Sound abspielen sollten. Wir möchten einen Sound spielen, wenn der Charakter auf eine Oberfläche stößt, aber im Moment kann er nur auf dem Boden aufschlagen, da die Kollision mit der Tilemap noch nicht implementiert ist.
Lassen Sie uns überprüfen, ob der Charakter gerade auf den Boden gefallen ist. Mit dem aktuellen Setup ist dies sehr einfach. Wir müssen nur nachsehen, ob sich der Charakter gerade auf dem Boden befindet, aber nicht im vorherigen Frame.
1 |
if (mOnGround && !mWasOnGround) |
2 |
mAudioSource.PlayOneShot(mHitWallSfx, 0.5f); |
Zum Schluss aktualisieren wir die vorherigen Eingaben.
1 |
UpdatePrevInputs(); |
Alles in allem sollte die CharacterUpdate-Funktion jetzt so aussehen, mit geringfügigen Unterschieden je nach Art der verwendeten Engine oder des verwendeten Frameworks.
1 |
public void CharacterUpdate() |
2 |
{
|
3 |
switch (mCurrentState) |
4 |
{
|
5 |
case CharacterState.Stand: |
6 |
|
7 |
mWalkSfxTimer = cWalkSfxTime; |
8 |
mAnimator.Play("Stand"); |
9 |
|
10 |
mSpeed = Vector2.zero; |
11 |
|
12 |
if (!mOnGround) |
13 |
{
|
14 |
mCurrentState = CharacterState.Jump; |
15 |
break; |
16 |
}
|
17 |
|
18 |
//if left or right key is pressed, but not both
|
19 |
if (KeyState(KeyInput.GoRight) != KeyState(KeyInput.GoLeft)) |
20 |
{
|
21 |
mCurrentState = CharacterState.Walk; |
22 |
break; |
23 |
}
|
24 |
else if (KeyState(KeyInput.Jump)) |
25 |
{
|
26 |
mSpeed.y = mJumpSpeed; |
27 |
mAudioSource.PlayOneShot(mJumpSfx); |
28 |
mCurrentState = CharacterState.Jump; |
29 |
break; |
30 |
}
|
31 |
|
32 |
break; |
33 |
case CharacterState.Walk: |
34 |
mAnimator.Play("Walk"); |
35 |
|
36 |
mWalkSfxTimer += Time.deltaTime; |
37 |
|
38 |
if (mWalkSfxTimer > cWalkSfxTime) |
39 |
{
|
40 |
mWalkSfxTimer = 0.0f; |
41 |
mAudioSource.PlayOneShot(mWalkSfx); |
42 |
}
|
43 |
|
44 |
//if both or neither left nor right keys are pressed then stop walking and stand
|
45 |
|
46 |
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft)) |
47 |
{
|
48 |
mCurrentState = CharacterState.Stand; |
49 |
mSpeed = Vector2.zero; |
50 |
break; |
51 |
}
|
52 |
else if (KeyState(KeyInput.GoRight)) |
53 |
{
|
54 |
if (mPushesRightWall) |
55 |
mSpeed.x = 0.0f; |
56 |
else
|
57 |
mSpeed.x = mWalkSpeed; |
58 |
mScale.x = -Mathf.Abs(mScale.x); |
59 |
}
|
60 |
else if (KeyState(KeyInput.GoLeft)) |
61 |
{
|
62 |
if (mPushesLeftWall) |
63 |
mSpeed.x = 0.0f; |
64 |
else
|
65 |
mSpeed.x = -mWalkSpeed; |
66 |
mScale.x = Mathf.Abs(mScale.x); |
67 |
}
|
68 |
|
69 |
//if there's no tile to walk on, fall
|
70 |
if (KeyState(KeyInput.Jump)) |
71 |
{
|
72 |
mSpeed.y = mJumpSpeed; |
73 |
mAudioSource.PlayOneShot(mJumpSfx, 1.0f); |
74 |
mCurrentState = CharacterState.Jump; |
75 |
break; |
76 |
}
|
77 |
else if (!mOnGround) |
78 |
{
|
79 |
mCurrentState = CharacterState.Jump; |
80 |
break; |
81 |
}
|
82 |
|
83 |
break; |
84 |
case CharacterState.Jump: |
85 |
|
86 |
mWalkSfxTimer = cWalkSfxTime; |
87 |
|
88 |
mAnimator.Play("Jump"); |
89 |
|
90 |
mSpeed.y += Constants.cGravity * Time.deltaTime; |
91 |
|
92 |
mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed); |
93 |
|
94 |
if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f) |
95 |
{
|
96 |
mSpeed.y = Mathf.Min(mSpeed.y, 200.0f); |
97 |
}
|
98 |
|
99 |
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft)) |
100 |
{
|
101 |
mSpeed.x = 0.0f; |
102 |
}
|
103 |
else if (KeyState(KeyInput.GoRight)) |
104 |
{
|
105 |
if (mPushesRightWall) |
106 |
mSpeed.x = 0.0f; |
107 |
else
|
108 |
mSpeed.x = mWalkSpeed; |
109 |
mScale.x = -Mathf.Abs(mScale.x); |
110 |
}
|
111 |
else if (KeyState(KeyInput.GoLeft)) |
112 |
{
|
113 |
if (mPushesLeftWall) |
114 |
mSpeed.x = 0.0f; |
115 |
else
|
116 |
mSpeed.x = -mWalkSpeed; |
117 |
mScale.x = Mathf.Abs(mScale.x); |
118 |
}
|
119 |
|
120 |
//if we hit the ground
|
121 |
if (mOnGround) |
122 |
{
|
123 |
//if there's no movement change state to standing
|
124 |
if (mInputs[(int)KeyInput.GoRight] == mInputs[(int)KeyInput.GoLeft]) |
125 |
{
|
126 |
mCurrentState = CharacterState.Stand; |
127 |
mSpeed = Vector2.zero; |
128 |
mAudioSource.PlayOneShot(mHitWallSfx, 0.5f); |
129 |
}
|
130 |
else //either go right or go left are pressed so we change the state to walk |
131 |
{
|
132 |
mCurrentState = CharacterState.Walk; |
133 |
mSpeed.y = 0.0f; |
134 |
mAudioSource.PlayOneShot(mHitWallSfx, 0.5f); |
135 |
}
|
136 |
}
|
137 |
break; |
138 |
|
139 |
case CharacterState.GrabLedge: |
140 |
break; |
141 |
}
|
142 |
|
143 |
UpdatePhysics(); |
144 |
|
145 |
if ((!mWasOnGround && mOnGround) |
146 |
|| (!mWasAtCeiling && mAtCeiling) |
147 |
|| (!mPushedLeftWall && mPushesLeftWall) |
148 |
|| (!mPushedRightWall && mPushesRightWall)) |
149 |
mAudioSource.PlayOneShot(mHitWallSfx, 0.5f); |
150 |
|
151 |
UpdatePrevInputs(); |
152 |
}
|
Initiere das Zeichen
Schreiben wir eine Init-Funktion für das Zeichen. Diese Funktion verwendet die Eingabearrays als Parameter. Wir werden diese später aus der Manager-Klasse liefern. Davon abgesehen müssen wir Dinge tun wie:
- Weisen Sie die Skala zu
- Weisen Sie die Sprunggeschwindigkeit zu
- Weisen Sie die Gehgeschwindigkeit zu
- Stellen Sie die Ausgangsposition ein
- Stellen Sie den AABB ein
1 |
public void CharacterInit(bool[] inputs, bool[] prevInputs) |
2 |
{
|
3 |
}
|
Wir werden hier einige der definierten Konstanten verwenden.
1 |
public const float cWalkSpeed = 160.0f; |
2 |
public const float cJumpSpeed = 410.0f; |
3 |
public const float cMinJumpSpeed = 200.0f; |
4 |
public const float cHalfSizeY = 20.0f; |
5 |
public const float cHalfSizeX = 6.0f; |
Im Fall der Demo können wir die Anfangsposition auf die Position im Editor setzen.
1 |
public void CharacterInit(bool[] inputs, bool[] prevInputs) |
2 |
{
|
3 |
mPosition = transform.position; |
4 |
}
|
Für den AABB müssen wir den Versatz und die halbe Größe einstellen. Der Versatz im Sprite der Demo muss nur halb so groß sein.
1 |
public void CharacterInit(bool[] inputs, bool[] prevInputs) |
2 |
{
|
3 |
mPosition = transform.position; |
4 |
mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY); |
5 |
mAABBOffset.y = mAABB.halfSize.y; |
6 |
}
|
Jetzt können wir uns um den Rest der Variablen kümmern.
1 |
public void CharacterInit(bool[] inputs, bool[] prevInputs) |
2 |
{
|
3 |
mPosition = transform.position; |
4 |
mAABB.halfSize = new Vector2(Constants.cHalfSizeX, Constants.cHalfSizeY); |
5 |
mAABBOffset.y = mAABB.halfSize.y; |
6 |
|
7 |
mInputs = inputs; |
8 |
mPrevInputs = prevInputs; |
9 |
|
10 |
mJumpSpeed = Constants.cJumpSpeed; |
11 |
mWalkSpeed = Constants.cWalkSpeed; |
12 |
|
13 |
mScale = Vector2.one; |
14 |
}
|
Wir müssen diese Funktion vom Spielmanager aus aufrufen. Der Manager kann auf viele Arten eingerichtet werden, abhängig von den von Ihnen verwendeten Werkzeuge. Im Allgemeinen ist die Idee jedoch dieselbe. In der Init des Managers müssen wir die Eingabearrays erstellen, einen Player erstellen und ihn initiieren.
1 |
public class Game |
2 |
{
|
3 |
public Character mPlayer; |
4 |
bool[] mInputs; |
5 |
bool[] mPrevInputs; |
6 |
|
7 |
void Start () |
8 |
{
|
9 |
inputs = new bool[(int)KeyInput.Count]; |
10 |
prevInputs = new bool[(int)KeyInput.Count]; |
11 |
|
12 |
player.CharacterInit(inputs, prevInputs); |
13 |
}
|
14 |
}
|
Zusätzlich müssen wir beim Update des Managers den Player und die Eingaben des Players aktualisieren.
1 |
void Update() |
2 |
{
|
3 |
inputs[(int)KeyInput.GoRight] = Input.GetKey(goRightKey); |
4 |
inputs[(int)KeyInput.GoLeft] = Input.GetKey(goLeftKey); |
5 |
inputs[(int)KeyInput.GoDown] = Input.GetKey(goDownKey); |
6 |
inputs[(int)KeyInput.Jump] = Input.GetKey(goJumpKey); |
7 |
}
|
8 |
|
9 |
void FixedUpdate() |
10 |
{
|
11 |
player.CharacterUpdate(); |
12 |
}
|
Beachten Sie, dass wir die Physik des Charakters im festen Update aktualisieren. Dadurch wird sichergestellt, dass die Sprünge immer gleich hoch sind, unabhängig davon, mit welcher Framerate unser Spiel arbeitet. Es gibt einen ausgezeichneten Artikel von Glenn Fiedler darüber, wie Sie den Zeitschritt korrigieren können, falls Sie Unity nicht verwenden.
Testen Sie das Zeichen Controller
An diesem Punkt können wir die Bewegung des Zeichens testen und sehen, wie es sich anfühlt. Wenn es uns nicht gefällt, können wir jederzeit die Parameter oder die Art und Weise ändern, wie die Geschwindigkeit bei Tastendruck geändert wird.



Zusammenfassung
Die Zeichensteuerung mag für manche sehr schwerelos und nicht so angenehm wie eine auf Momentum basierende Bewegung erscheinen, aber dies ist alles eine Frage der Art der Steuerung, die am besten zu Ihrem Spiel passt. Glücklicherweise ist es ziemlich einfach, die Art und Weise zu ändern, in der sich das Zeichen bewegt. Es reicht aus, zu ändern, wie sich der Geschwindigkeitswert im Geh- und Sprungzustand ändert.
Das war's für den ersten Teil der Serie. Wir haben ein einfaches Bewegungsschema für Zeichen entwickelt, aber nicht viel mehr. Das Wichtigste ist, dass wir den Weg für den nächsten Teil festgelegt haben, in dem wir den Charakter mit einer Tilemap interagieren lassen.