Dasar Physics Platformer 2D, Bagian 1
() translation by (you can also view the original English article)
Tabrakan Karakter
Oke, jadi begini ceritanya: kita ingin membuat platformer 2D dengan sistem simulasi physics yang sederhana, responsif, akurat, dan bisa ditebak. Kita tidak mau menggunakan physics engine 2D yang besar dalam game ini, dengan beberapa alasan:
- respon tabrakan yang tidak bisa ditebak
- pergerakan karakter yang sulit diatur tidak akurat
- lebih rumit untuk digunakan
- menggunakan lebih banyak daya proses dibanding physics sederhana
Tentu saja ada juga keuntungan menggunakan physics engine yang sudah jadi, misalnya bisa mengatur interaksi physics yang rumit dengan cukup mudah, tapi hal itu tidak dibutuhkan di game yang akan kita buat.
Physics engine buatan kita sendiri akan membantu game kita memberi pengalaman bermain yang unik dan berbeda dari yang lain, dan itu sangat penting! Walaupun kamu memulai dengan pengaturan yang dasar, bagaimana benda-benda bergerak dan berinteraksi satu sama lain akan dipengaruhi oleh peraturan yang kamu buat sendiri, bukan dibuat oleh orang lain. Ayo kita mulai!
Batas Karakter
Kita mulai dengan menentukan bentuk apa yang akan kita gunakan pada sistem physics kita. Salah satu bentuk yang paling umum digunakan untuk merepresentasikan objek fisik dalam sebuah game adalah Axis Aligned Bounding Box (AABB). AABB pada dasarnya adalah persegi yang tidak berputar.



Dalalm banyak game platformer, AABB sudah cukup sebagai perkiraan badan berbagai objek dalam game. AABB sangat efektif karena sangat mudah untuk memperhitungkan tumpang tindih antar AABB dan hanya membutuhkan sedikit data. Untuk mendeskripsikan AABB, kita hanya butuh tahu titik pusat dan ukurannya.
Tanpa berlama-lama, ayo buat struktur data untuk AABB kita.
1 |
public struct AABB |
2 |
{
|
3 |
}
|
Seperti yang disebutkan sebelumnya, yang kita perlukan hanyalah dua vektor; yang pertama adalah pusat AABB, dan yang kedua adalah half size, setengah dari ukuran persegi AABB. Kenapa setengah dari ukuran kotak? Sebagian besar perhitungan membutuhkan nilai setengah dari ukuran kotak, jadi daripada kita hitung setiap kali, kita cukup menyimpannya sebagai data.
1 |
public struct AABB |
2 |
{
|
3 |
public Vector2 center; |
4 |
public Vector2 halfSize; |
5 |
}
|
Mari mulai dengan membuat konstruktor, agar bisa membuat struktur data dengan parameter tertentu.
1 |
public AABB(Vector2 center, Vector2 halfSize) |
2 |
{
|
3 |
this.center = center; |
4 |
this.halfSize = halfSize; |
5 |
}
|
Dengan ini kita bisa membuat fungsi pemeriksaan tabrakan. Pertama, kita lakukan pemeriksaan sederhana apakah kedua AABB saling bertabrakan. Hal ini sangat sederhana, kita cukup melihat apakah jarak antara kedua pusat di masing-masing sumbu lebih kecil dari total nilai half size mereka.
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 |
}
|
Berikut adalah gambar pemeriksaan pada sumbu x, pemeriksaan pada sumbu y dilakukan dengan cara yang serupa.



Seperti kamu lihat, jika total half size lebih kecil dari jarak antar pusat, tidak mungkin terjadi tumpang tindih. Perhatikan bahwa pada kode di atas, kita bisa keluar dari pemeriksaan tabrakan lebih awal jika kita tahu kedua objek tidak tumpang tindih di sumbu pertama. Tumpang tindih tersebut harus terjadi pada dua sumbu, jika AABB tersebut bertabrakan pada dunia 2D.
Menggerakkan Objek
Mari mulai dengan membuat sebuah kelas untuk objek yang dipengaruhi oleh physics dalam game. Nantinya, kita akan menggunakan kelas ini sebagai dasar untuk objek pemain. Kita sebut kelas ini MovingObject.
1 |
public class MovingObject |
2 |
{
|
3 |
}
|
Sekarang kita isi kelas ini dengan data. Kita akan membutuhkan cukup banyak informasi untuk objek ini.
- Posisi dan posisi pada frame sebelumnya
- kecepatan dan kecepatan pada frame sebelumna
- skala
- AABB dan offsetnya (agar kita bisa selaraskan dengan sprite)
- apakah objek tersebut ada di atas tanah, dan apakah ada di atas tanah di frame sebelumnya
- apakah di sebelah kiri objek ada tembok dan kondisi tersebut di frame sebelumnya
- apakah di sebelah kanan objek ada tembok dan kondisi tersebut di frame sebelumnya
- apakah objek berada di langit-langit dan kondisi tersebut di frame sebelumnya
Posisi, kecepatan, dan skala adalah vektor 2D.
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 |
}
|
Sekarang tambahkan AABB dan offsetnya. Offset dibutuhkan agar kita bisa mencocokkan AABB dan sprite objek sesuai kebutuhan.
1 |
public AABB mAABB; |
2 |
public Vector2 mAABBOffset; |
Lalu, buat variabel yang akan menunjukkan kondisi posisi objek, apakah ada di atas tanah, di sebelah tembok, atau di langit-langit. Variabel-variabel tersebut sangat penting karena mereka akan memberitahu apakah kita melompat atau misalnya kita perlu memainkan suara saat menabrak tembok.
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; |
Ini adalah dasar-dasarnya. Sekarang kita buat fungsi yang akan mengupdate objek. Untuk saat ini kita tidak akan mengatur semuanya, hanya secukupnya agar kita bisa mulai kontrol dasar untuk karakter.
1 |
public void UpdatePhysics() |
2 |
{
|
3 |
}
|
Hal pertama yang perlu dilakukan adalah menyimpan data dari frame sebelumnya ke variabel yang sesuai.
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 |
}
|
Sekarang kita update posisi sesuai dengan kecepatan saat ini.
1 |
mPosition += mSpeed*Time.deltaTime; |
Khusus untuk saat ini saja, kita buat jika posisi vertikal kurang dari nol, kita asumsikan karakter ada di tanah. Hal ini hanya sementara, agar kita bisa mengatur kontrol karakter. Nantinya akan kita periksa tabrakan menggunakan tilemap.
1 |
if (mPosition.y < 0.0f) |
2 |
{
|
3 |
mPosition.y = 0.0f; |
4 |
mOnGround = true; |
5 |
}
|
6 |
else
|
7 |
mOnGround = false; |
Setelah ini, kita perlu update pusat AABB, agar sesuai dengan posisi baru.
1 |
mAABB.center = mPosition + mAABBOffset; |
Untuk project demo ini, saya menggunakan Unity. Untuk mengupdate posisi objek kita perlu mengaplikasikannya pada komponen transform. Hal yang sama perlu dilakukan untuk skala.
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); |
Seperti yang bisa kamu lihat, posisi hasil render dibulatkan ke atas. Ini untuk memastikan katakter yang dirender selalu menempel pada sebuah pixel.
Kontrol Karakter
Data
Karena sekarang kita sudah memiliki kelas MovingObjek dasar, kita bisa mulai bermain dengan pergerakan karakter. Ini adalah bagian yang sangat penting dari game, dan bisa diselesaikan secepat mungkin, tidak perlu masuk terlalu dalam ke sistem dalam game, dan akan siap saat kita perlu menguji tabrakan antara karakter dan peta.
Pertama, kita buat kelas karakter dan menurunkannya dari kelas MovingObject.
1 |
public class Character : MovingObject |
2 |
{
|
3 |
}
|
Kita perlu menangani beberapa hal di sini. Pertama, input. Kita buat sebuah enum yang mencakup semua kontrol untuk karakter. Kita buat di file lain dan beri nama KeyInput.
1 |
public enum KeyInput |
2 |
{
|
3 |
GoLeft = 0, |
4 |
GoRight, |
5 |
GoDown, |
6 |
Jump, |
7 |
Count
|
8 |
}
|
Seperti yang kamu lihat, karakter kita bisa bergerak ke kiri, kanan, bawah, dan melompat ke atas. Berjalan ke bawah hanya akan bekerja pada platform satu arah, saat kita ingin jatuh menembusnya.
Sekarang kita deklarasi dua array di kelas Character, satu untuk input frame ini, dan satunya untuk input frame sebelumnya. Tergantung pada game yang bersangkutan, pengaturan ini bisa masuk akal atau tidak. Biasanya, daripada menyimpan kondisi tombol di array, kondisi tombol diperiksa sesuai kebutuhan menggunakan fungsi spesifik dari engine atau framework. Tapi, memiliki array yang tidak terikat ke input asli bisa menguntungkan, misalnya jika kita ingin mensimulasikan sebuah tombol ditekan.
1 |
protected bool[] mInputs; |
2 |
protected bool[] mPrevInputs; |
Array-array ini akan diindex berdasarkan enum KeyInput. Untuk menggunakan array-array tersebut dengan mudah, kita buat beberapa fungsi untuk membantu kita memeriksa sebuah tombol.
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 |
}
|
Tidak ada yang khusus di sini, kita ingin melihat apakah sebuah tombol baru ditekan, baru dilepas, atau apakah tombol itu aktif atau tidak.
Sekarang buat sebuah enum lain yang akan menyimpan semua kondisi karakter.
1 |
public enum CharacterState |
2 |
{
|
3 |
Stand, |
4 |
Walk, |
5 |
Jump, |
6 |
GrabLedge, |
7 |
};
|
Seperti yang bisa kamu lihat, karakter kita bisa berdiri diam, berjalan, melompat, atau berpegangan di tebing. Setelah ini selesai, kita perlu menambahkan variabel seperti kecepatan melompat, kecepatan berjalan, dan kondisi saat ini.
1 |
public CharacterState mCurrentState = CharacterState.Stand; |
2 |
public float mJumpSpeed; |
3 |
public float mWalkSpeed; |
Tentu saja ada beberapa data lain yang dibutuhkan di sini, seperti sprite karakter, tapi bagaimana melakukannya akan sangat tergantung pada engine yang akan kamu gunakan. Karena saya menggunakan Unity, saya akan menggunakan referensi ke sebuah Animator untuk memastikakn sprite menjalankan animasi untuk kondisi yang sesuai.
Loop Update
Sekarang kita bisa mulai mengerjakan loop update. Yang akan kita lakukan dalam loop tergantung dengan kondisi karakter saat ini.
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 |
}
|
Kondisi Berdiri
Kita mulai dengan mengisi apa yang harus dilakukan saat karakter sedang tidak bergerak dalam kondisi berdiri. Pertama, kecepatan harus diatur menjadi nol.
1 |
case CharacterState.Stand: |
2 |
mSpeed = Vector2.zero; |
3 |
break; |
Kita juga ingin menampilkan sprite yang sesuai untuk kondisi tersebut.
1 |
case CharacterState.Stand: |
2 |
mSpeed = Vector2.zero; |
3 |
mAnimator.Play("Stand"); |
4 |
break; |
Lalu jika karakter tidak ada di atas tanah, karakter tidak bisa berdiri, jadi kita perlu ganti kondisinya menjadi melompat.
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; |
Jika tombol GoLeft atau GoRight ditekan, kita perlu mengubah kondisinya menjadi berjalan.
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; |
Jika tombol Jump ditekan, kita perlu mengatur kecepatan vertikal manjadi kecepatan melompat, dan mengubah kondisinya menjadi melompat.
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 |
}
|
Sejauh ini penanganan kondisi tersebut sudah cukup.
Kondisi Berjalan
Sekarang buat logika untuk bergerak di atas tanah, dan langsung menjalankan animasi berjalan.
1 |
case CharacterState.Walk: |
2 |
mAnimator.Play("Walk"); |
3 |
break; |
Jika kita tidak menekan tombol kiri atau kanan, atau jika kedua tombol ditekan bersamaan, kita perlu kembali ke kondisi berdiri diam.
1 |
if (KeyState(KeyInput.GoRight) == KeyState(KeyInput.GoLeft)) |
2 |
{
|
3 |
mCurrentState = CharacterState.Stand; |
4 |
mSpeed = Vector2.zero; |
5 |
break; |
6 |
}
|
Jika tombol GoRight ditekan, kita atur kecepatan horizontal menjadi mWalkSpeed dan pastikan sprite diatur skalanya dengan benar. Skala horizontal perlu diubah jika kita mau memutar sprite secara horizontal.
Kita hanya boleh bergerak jika tidak ada rintangan di depan karakter, jadi jika mPushesRightWall nilainya true, maka kecepatan horizontal perlu diatur jadi nol jika kita bergerak ke kanan.
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 |
}
|
Kita juga menangani arah kiri dengan cara yang sama.
Seperti pada kondisi berdiri, kita perlu periksa apakah tombol melompat ditekan, jika iya kita atur kecepatan vertikal sesuai kecepatan lompat.
1 |
if (KeyState(KeyInput.Jump)) |
2 |
{
|
3 |
mSpeed.y = mJumpSpeed; |
4 |
mAudioSource.PlayOneShot(mJumpSfx, 1.0f); |
5 |
mCurrentState = CharacterState.Jump; |
6 |
break; |
7 |
}
|
Jika karakter tidak di atas tanah, maka kita perlu ubah kondisi karakter menjadi lompat tapi tanpa mengubah kecepatan vertikal, jadi karakter akan bergerak jatuh ke bawah.
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 |
}
|
Cukup sekian untuk kondisi berjalan. Sekarang kita lanjutkan ke kondisi melompat.
Kondisi Melompat
Kita mulai dengan mengatur animasi yang sesuai untuk sprite.
1 |
mAnimator.Play("Jump"); |
Pada kondisi melompat, kita perlu menambahkan gravitasi pada kecepatan karakter, jadi karakter akan bergerak semakin lama semakin cepat ke arah tanah.
1 |
mSpeed.y += Constants.cGravity * Time.deltaTime; |
Tapi perlu kita tambahkan suatu batasan agar karakter tidak bergerak jatuh terlalu cepat.
1 |
mSpeed.y = Mathf.Max(mSpeed.y, Constants.cMaxFallingSpeed); |
Dalam banyak game, jika karakter ada di udara, kemampuan bergeraknya akan berkurang, tapi kita akan menggunakan kontrol yang sangat sederhana dan akurat untuk memberikan fleksibilitas penuh saat karakter ada di udara. Jadi kita kita tekan tombol GoLeft atau GoRight, karakter akan bergerak ke arah tersebut sambil melompat, sama cepatnya seperti jika karakter ada di atas tanah. Dalam hal ini, kita bisa salin logika pergerakan dari kondisi berjalan.
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 |
} |
Akhirnya kita akan membuat karakter melompat lebih tinggi jika tombol melompat ditekan lebih lama. Untuk melakukan ini, yang perlu kita lakukan adalah membuat lompatan karakter lebih rendah jika tombol melompat tidak ditekan.
1 |
if (!KeyState(KeyInput.Jump) && mSpeed.y > 0.0f) |
2 |
mSpeed.y = Mathf.Min(mSpeed.y, Constants.cMinJumpSpeed); |
Seperti yang bisa kamu lihat, jika tombol melompat tidak ditekan dan kecepatan vertikal bernilai positif, maka kita batasi kecepatan ke nilai maksimum cMinJumpSpeed
(200 piksel per detik). Ini artinya jika kita hanya menekan tombol melompat sesaat, kecepatan lompat akan diturunkan menjadi 200, bukan bernilai mJumpSpeed
(yang nilai awalnya 410), sehingga karakter akan melompat lebih rendah.
Karena kita belum memiliki geometri level, kita perlu melewatkan implementasi GrabLedge untuk sementara.
Update Input Sebelumnya
Saat semua proses pada frame selesai, kita bisa update nilai input sebelumnya. Mari buat fungsi baru untuk ini. Yang kita perlu lakukan di sini adalah memindahkan nilai key state dari array mInputs
ke array mPrevInputs
.
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 |
}
|
Di akhir fungsi CharacterUpdate, kita masih perlu melakukan beberapa hal. Pertama, update physics.
1 |
UpdatePhysics(); |
Sekarang karena physics sudah diupdate, periksa apakah kita perlu memainkan suatu suara atau tidak. Kita ingin memainkan suara saat karakter menabrak permukaan apapun, tapi saat ini karakter hanya bisa menabrak permukaan tanah karena tabrakan dengan tilemap belum diimplementasi.
Mari periksa apakah karakter jauh ke permukaan tanah. Hal ini sangat mudah untuk dilakukan dengan pengaturan saat ini, kita hanya perlu memeriksa apakah saat ini karakter ada di atas tanah, tapi tidak begitu di frame sebelumnya.
1 |
if (mOnGround && !mWasOnGround) |
2 |
mAudioSource.PlayOneShot(mHitWallSfx, 0.5f); |
Lalu, kita update input sebelumnya.
1 |
UpdatePrevInputs(); |
Kurang lebih seperti ini lah seharusnya fungsi CharacterUpdate, dengan perubahan kecil tergantung dengan engine atau framework yang kamu gunakan.
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 |
}
|
Inisialisasi Karakter
Mari buat sebuah fungsi inisialisasi untuk karakter. Fungsi ini akan menggunakan array input sebagai parameter. Nantinya kita akan menyediakan parameter tersebut dari kelas manajer. Selain itu, kita perlu melakukan hal-hal berikut:
- tentukan nilai skala
- tentukan kecepatan melompat
- tentukan kecepatan berjalan
- atur posisi awal
- atur AABB
1 |
public void CharacterInit(bool[] inputs, bool[] prevInputs) |
2 |
{
|
3 |
}
|
Kita akan menggunakan beberapa definisi konstanta.
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; |
Dalam hal demo ini, kita akan atur nilai posisi awal menjadi posisi di editor.
1 |
public void CharacterInit(bool[] inputs, bool[] prevInputs) |
2 |
{
|
3 |
mPosition = transform.position; |
4 |
}
|
Untuk AABB, kita perlu atur nilai offset dan half size. Offset untuk sprite dalam demo ini adalah nilai half size.
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 |
}
|
Sekarang kita bisa mengurus sisa variabel yang ada.
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 |
}
|
Kita perlu memanggil fungsi ini dari game manager. Manajer ini bisa diatur dengan berbagai cara, tergantung dari engine yang kamu gunakan, tapi ide dasarnya kurang lebih sama. Pada inisialisasi manajer, kita perlu buat array input, buat sebuah objek pemain, dan menginisialisasinya.
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 |
}
|
Lalu pada update manajer, kita perlu update pemain dan array input pemain.
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 |
}
|
Perhatikan bahwa kita mengupdate physics karakter dalam update dengan jeda yang tetap. Ini akan memastikan aksi melompat akan selalu dengan ketinggian yang sama, tidak masalah berapa frame rate saat game kita dijalankan. Ada artikel yang bagus dari Glenn Fiedler untuk bagaimana memperbaiki timestep jika kamu tidak menggunakan Unity.
Menguji Controller Karakter
Di titik ini kita bisa menguji pergerakan karakter apakah sudah enak dilihat atau belum. Jika kita tidak menyukai hasil saat ini, kita bisa mengubah berbagai parameter atau bagaimana nilai kecepatan berubah saat tombol ditekan.



Ringkasan
Kontrol karakter mungkin terlihat tanpa beban dan tidak senyaman dibandingkan pergerakan berbasis momentum untuk sebagian orang, tapi ini semua tergantung dengan kontrol seperti apa yang paling cocok dengan game yang ingin kamu buat. Untungnya untuk mengubah bagaimana karakter bergerak cukup mudah: hanya dengan mengubah bagaimana nilai kecepatan berubah dalam kondisi berjalan dan melompat.
Sekian tutorial bagian pertama dari seri ini. Kita sudah memiliki skema pergerakan karakter, tidak lebih dari itu. Yang paling penting adalah kita sudah memiliki landasan untuk bagian berikutnya, di mana kita akan membuat karakter berinteraksi dengan tilemap.