Dasar Physics Platformer 2D, Bagian 3
() translation by (you can also view the original English article)
Platform Satu Arah
Karena kita baru menyelesaikan pemeriksaan tabrakan dengan tanah, sekalian saja kita tambahkan platform satu arah. Mereka hanya akan mempengaruhi pemeriksaan tabrakan dengan tanah saja. Perbedaan platform satu arah dengan blok solid adalah bahwa platform satu arah hanya menghentikan objek jika jatuh dari atas. Sebagai tambahan, kita juga perlu membolehkan karakter untuk melompat turun dari platform tersebut.
Pertama, saat kita ingin melompat turun dari platform satu arah, pada dasarnya kita mengabaikan tabrakan dengan tanah. Cara mudah untuk melakukan itu adalah dengan membuat offset, setelah melewati offset tersebut, karakter atau objek tidak lagi bertabrakan dengan platform.
Contohnya, jika karakter sudah berada di bawah platform sebanyak dua piksel, seharusnya tidak dideteksi sebagai tabrakan. Dengan begitu, jika kita ingin turun dari platform, kita hanya perlu menggeser karakter dua piksel ke bawah. Mari buat konstanta offset tersebut.
1 |
public const float cOneWayPlatformThreshold = 2.0f; |
Sekarang kita tambahkan variabel yang akan memberi tahu kita apakah sebuah objek berada pada platform satu arah.
1 |
public bool mOnOneWayPlatform = false; |
Lalu kita modifikasi definisi fungsi HasGround
untuk menyimpan referensi ke variabel boolean yang akan diisi jika objek mendarat pada platform satu arah.
1 |
public bool HasGround(Vector2 oldPosition, Vector2 position, Vector2 speed, out float groundY, ref bool onOneWayPlatform) |
Sekarang, setelah kita periksa apakah petak yang kita injak adalah rintangan atau bukan, kita perlu periksa apakah itu adalah sebuah platform satu arah.
1 |
if (mMap.IsObstacle(tileIndexX, tileIndexY)) |
2 |
return true; |
3 |
else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY)) |
4 |
onOneWayPlatform = true; |
Seperti yang dijelaskan sebelumnya, kita juga perlu memastikan tabrakan diabaikan jika kita sudah jatuh melewati cOneWayPlatformThreshold
di bawah platform.
Tentu saja, kita tidak bisa hanya membandingkan perbedaan antara bagian atas petak dan sensor, karena mudah untuk membayangkan jika kita sedang jatuh, kita akan melewati batas dua piksel dari bagian atas platform. Untuk platform satu arah menghentikan objek, kita ingin jarak antara bagian atas petak dan sensor lebih kecil atau sama dengan cOneWayPlatformThreshold
ditambah offset dari posisi frame ini dibandingkan dengan posisi frame sebelumnya.
1 |
if (mMap.IsObstacle(tileIndexX, tileIndexY)) |
2 |
return true; |
3 |
else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY) |
4 |
&& Mathf.Abs(checkedTile.y - groundY) <= Constants.cOneWayPlatformThreshold + mOldPosition.y - position.y) |
5 |
onOneWayPlatform = true; |
Akhirnya, ada satu hal lagi yang perlu dipertimbangkan. Ketika kita menemukan platform satu arah, kita tidak bisa benar-benar keluar dari loop, karena ada situasi di mana sebagian karakter ada di atas platform dan sebagian di atas blok solid.



Kita sebaiknya tidak menganggap posisi tersebut seperti 'platform satu arah', karena kita tidak bisa benar-benar turun dari situ, blok solid itu menghentikan kita. Karena itulah kita perlu terus mencari blok solid, dan jika kita menemukan satu sebelum kita mengembalikan hasil fungsi, kita perlu mengatur onOneWayPlatform
menjadi false.
1 |
if (mMap.IsObstacle(tileIndexX, tileIndexY)) |
2 |
{
|
3 |
onOneWayPlatform = false; |
4 |
return true; |
5 |
}
|
6 |
else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY) |
7 |
&& Mathf.Abs(checkedTile.y - groundY) <= Constants.cOneWayPlatformThreshold + mOldPosition.y - position.y) |
8 |
onOneWayPlatform = true; |
Sekarang, jika kita memeriksa semua petak yang perlu kita periksa secara horizontal dan menemukan platform satu arah tapi tidak ada blok solid, maka kita bisa yakin kalau kita berada pada platform satu arah dan kita bisa melompat turun.
1 |
if (mMap.IsObstacle(tileIndexX, tileIndexY)) |
2 |
{
|
3 |
onOneWayPlatform = false; |
4 |
return true; |
5 |
}
|
6 |
else if (mMap.IsOneWayPlatform(tileIndexX, tileIndexY) |
7 |
&& Mathf.Abs(checkedTile.y - groundY) <= Constants.cOneWayPlatformThreshold + mOldPosition.y - position.y) |
8 |
onOneWayPlatform = true; |
9 |
|
10 |
if (checkedTile.x >= bottomRight.x) |
11 |
{
|
12 |
if (onOneWayPlatform) |
13 |
return true; |
14 |
break; |
15 |
}
|
Sekarang tambahkan kelas karakter cara untuk melompat turun dari platform. Di kondisi berdiri dan berlari, kita perlu tambahkan kode berikut.
1 |
if (KeyState(KeyInput.GoDown)) |
2 |
{
|
3 |
if (mOnOneWayPlatform) |
4 |
mPosition.y -= Constants.cOneWayPlatformThreshold; |
5 |
}
|
Kita lihat bagaimana fungsi itu bekerja.



Semuanya bekerja seperti seharusnya.
Menangani Tabrakan dengan Langit-langit
Kita perlu buat fungsi yang serupa dengan HasGround untuk masing-masing sisi AABB, kita mulai dengan langit-langit. Perbedaannya adalah sebagai berikut:
- Garis sensor berada di atas AABB, bukan di bawahnya.
- Kita periksa petak langit-langit dari bawah ke atas karena kita bergerak ke atas
- Tidak perlu menangani platform satu arah
Berikut adalah fungsi yang sudah dimodifikasi.
1 |
public bool HasCeiling(Vector2 oldPosition, Vector2 position, out float ceilingY) |
2 |
{
|
3 |
var center = position + mAABBOffset; |
4 |
var oldCenter = oldPosition + mAABBOffset; |
5 |
|
6 |
ceilingY = 0.0f; |
7 |
|
8 |
var oldTopRight = oldCenter + mAABB.halfSize + Vector2.up - Vector2.right; |
9 |
|
10 |
var newTopRight = center + mAABB.halfSize + Vector2.up - Vector2.right; |
11 |
var newTopLeft = new Vector2(newTopRight.x - mAABB.halfSize.x * 2.0f + 2.0f, newTopRight.y); |
12 |
|
13 |
int endY = mMap.GetMapTileYAtPoint(newTopRight.y); |
14 |
int begY = Mathf.Min(mMap.GetMapTileYAtPoint(oldTopRight.y) + 1, endY); |
15 |
int dist = Mathf.Max(Mathf.Abs(endY - begY), 1); |
16 |
|
17 |
int tileIndexX; |
18 |
|
19 |
for (int tileIndexY = begY; tileIndexY <= endY; ++tileIndexY) |
20 |
{
|
21 |
var topRight = Vector2.Lerp(newTopRight, oldTopRight, (float)Mathf.Abs(endY - tileIndexY) / dist); |
22 |
var topLeft = new Vector2(topRight.x - mAABB.halfSize.x * 2.0f + 2.0f, topRight.y); |
23 |
|
24 |
for (var checkedTile = topLeft; ; checkedTile.x += Map.cTileSize) |
25 |
{
|
26 |
checkedTile.x = Mathf.Min(checkedTile.x, topRight.x); |
27 |
|
28 |
tileIndexX = mMap.GetMapTileXAtPoint(checkedTile.x); |
29 |
|
30 |
if (mMap.IsObstacle(tileIndexX, tileIndexY)) |
31 |
{
|
32 |
ceilingY = (float)tileIndexY * Map.cTileSize - Map.cTileSize / 2.0f + mMap.mPosition.y; |
33 |
return true; |
34 |
}
|
35 |
|
36 |
if (checkedTile.x >= topRight.x) |
37 |
break; |
38 |
}
|
39 |
}
|
40 |
|
41 |
return false; |
42 |
}
|
Menangani Tabrakan untuk Tembok Kiri
Sama seperti cara menangani tabrakan terhadap langit-langit dan tanah, kita juga perlu memeriksa apakah objek bertabrakan dengan tembok di kanan atau kiri. Kita mulai dari tembok sebelah kiri. Caranya kurang lebih sama, tapi berikut beberapa perbedaannya:
- Garis sensor ada di sebelah kiri AABB
- Loop
for
dalam perlu mengiterasi petak secara vertikel, karena sensor sekarang berupa garis vertikal. - Loop luar perlu mengiterasi petak secara horizontal untuk melihat apakah kita melewati tembok saat bergerak dengan kecepatan horizontal yang tinggi.
1 |
public bool CollidesWithLeftWall(Vector2 oldPosition, Vector2 position, out float wallX) |
2 |
{
|
3 |
var center = position + mAABBOffset; |
4 |
var oldCenter = oldPosition + mAABBOffset; |
5 |
|
6 |
wallX = 0.0f; |
7 |
|
8 |
var oldBottomLeft = oldCenter - mAABB.halfSize - Vector2.right; |
9 |
var newBottomLeft = center - mAABB.halfSize - Vector2.right; |
10 |
var newTopLeft = newBottomLeft + new Vector2(0.0f, mAABB.halfSize.y * 2.0f); |
11 |
|
12 |
int tileIndexY; |
13 |
|
14 |
var endX = mMap.GetMapTileXAtPoint(newBottomLeft.x); |
15 |
var begX = Mathf.Max(mMap.GetMapTileXAtPoint(oldBottomLeft.x) - 1, endX); |
16 |
int dist = Mathf.Max(Mathf.Abs(endX - begX), 1); |
17 |
|
18 |
for (int tileIndexX = begX; tileIndexX >= endX; --tileIndexX) |
19 |
{
|
20 |
var bottomLeft = Vector2.Lerp(newBottomLeft, oldBottomLeft, (float)Mathf.Abs(endX - tileIndexX) / dist); |
21 |
var topLeft = bottomLeft + new Vector2(0.0f, mAABB.halfSize.y * 2.0f); |
22 |
|
23 |
for (var checkedTile = bottomLeft; ; checkedTile.y += Map.cTileSize) |
24 |
{
|
25 |
checkedTile.y = Mathf.Min(checkedTile.y, topLeft.y); |
26 |
|
27 |
tileIndexY = mMap.GetMapTileYAtPoint(checkedTile.y); |
28 |
|
29 |
if (mMap.IsObstacle(tileIndexX, tileIndexY)) |
30 |
{
|
31 |
wallX = (float)tileIndexX * Map.cTileSize + Map.cTileSize / 2.0f + mMap.mPosition.x; |
32 |
return true; |
33 |
}
|
34 |
|
35 |
if (checkedTile.y >= topLeft.y) |
36 |
break; |
37 |
}
|
38 |
}
|
39 |
|
40 |
return false; |
41 |
}
|
Menangani Tabrakan dengan Tembok di Kanan
Akhirnya, kita buat fungsi CollidesWithRightWall
, seperti yang bisa kamu bayangkan, akan melakukan hal yang serupa dengan CollidesWithLeftWall
, tapi kita menggunakan sensor di sebelah kanan karakter, bukan kiri.
Perbedaan lainnya adalah kita akan memeriksa petak dari kiri ke kanan, karena gerakan karakter akan seperti itu.
1 |
public bool CollidesWithRightWall(Vector2 oldPosition, Vector2 position, out float wallX) |
2 |
{
|
3 |
var center = position + mAABBOffset; |
4 |
var oldCenter = oldPosition + mAABBOffset; |
5 |
|
6 |
wallX = 0.0f; |
7 |
|
8 |
var oldBottomRight = oldCenter + new Vector2(mAABB.halfSize.x, -mAABB.halfSize.y) + Vector2.right; |
9 |
var newBottomRight = center + new Vector2(mAABB.halfSize.x, -mAABB.halfSize.y) + Vector2.right; |
10 |
var newTopRight = newBottomRight + new Vector2(0.0f, mAABB.halfSize.y * 2.0f); |
11 |
|
12 |
var endX = mMap.GetMapTileXAtPoint(newBottomRight.x); |
13 |
var begX = Mathf.Min(mMap.GetMapTileXAtPoint(oldBottomRight.x) + 1, endX); |
14 |
int dist = Mathf.Max(Mathf.Abs(endX - begX), 1); |
15 |
|
16 |
int tileIndexY; |
17 |
|
18 |
for (int tileIndexX = begX; tileIndexX <= endX; ++tileIndexX) |
19 |
{
|
20 |
var bottomRight = Vector2.Lerp(newBottomRight, oldBottomRight, (float)Mathf.Abs(endX - tileIndexX) / dist); |
21 |
var topRight = bottomRight + new Vector2(0.0f, mAABB.halfSize.y * 2.0f); |
22 |
|
23 |
for (var checkedTile = bottomRight; ; checkedTile.y += Map.cTileSize) |
24 |
{
|
25 |
checkedTile.y = Mathf.Min(checkedTile.y, topRight.y); |
26 |
|
27 |
tileIndexY = mMap.GetMapTileYAtPoint(checkedTile.y); |
28 |
|
29 |
if (mMap.IsObstacle(tileIndexX, tileIndexY)) |
30 |
{
|
31 |
wallX = (float)tileIndexX * Map.cTileSize - Map.cTileSize / 2.0f + mMap.mPosition.x; |
32 |
return true; |
33 |
}
|
34 |
|
35 |
if (checkedTile.y >= topRight.y) |
36 |
break; |
37 |
}
|
38 |
}
|
39 |
|
40 |
return false; |
41 |
}
|
Menggerakkan Objek Keluar dari Tabrakan
Semua fungsi deteksi tabrakan sudah selesai, sekarang kita gunakan mereka untuk melengkapi respon tabrakan terhadap tilemap. Sebelum itu, kita perlu menentukan urutan pemeriksaan tabrakan. Kita pertimbangkan beberapa situasi berikut.



Di kedua situasi tersebut, kita bisa lihat karakter berakhir tumpang tindih dengan petak, tapi kita perlu menentukan bagaimana kita menyelesaikan tumpang tindih tersebut.
Situasi di kiri cukup sederhana, kita bisa lihat bahwa kita jatuh lurus ke bawah, dan karena itu kita harus mendarat di atas blok.
Situasi di kanan lebih rumit, karena sebenarnya kita bisa mendarat di pojok petak, dan mendorong karakter ke atas sama masuk akalnya dengan mendorongnya ke kanan. Mari pilih untuk memprioritaskan gerakan horizontal. Tidak terlalu masalah arah mana yang kita lakukan terlebih dahulu, kedua pilihan tersebut terlihat benar pada prakteknya.
Mari berpindah ke fungsi UpdatePhysics
dan tambahkan variabel yang akan menyimpan hasil dari pemeriksaan tabrakan.
1 |
float groundY = 0.0f, ceilingY = 0.0f; |
2 |
float rightWallX = 0.0f, leftWallX = 0.0f; |
Kita mulai dengan melihat apakah kita perlu menggeser objek ke kanan. Kondisinya adalah sebagai berikut:
- kecepatan horizontal lebih kecil atau sama dengan nol
- kita menabrak tembok kiri
- di frame sebelumnya kita tidak tumpang tindih dengan petak pada sumbu horizontal, situasi yang mirip dengan situasi sisi kanan pada gambar di atas
Kondisi terakhir adalah yang paling penting, karena jika tidak terpenuhi kita akan berhadapan dengan situasi yang serupa dengan sisi kiri dari gambar di atas, di mana kita seharusnya tidak menggerakkan karakter ke kanan.
1 |
if (mSpeed.x <= 0.0f |
2 |
&& CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX) |
3 |
&& mOldPosition.x - mAABB.halfSize.x + mAABBOffset.x >= leftWallX) |
4 |
{
|
5 |
}
|
Jika kondisinya benar, kita perlu menyamakan sisi kiri AABB kita dengan sisi kanan dari petak, pastikan kita berhenti bergerak ke kiri, dan tandai bahwa ada tembok di sisi kiri kita.
1 |
if (mSpeed.x <= 0.0f |
2 |
&& CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX) |
3 |
&& mOldPosition.x - mAABB.halfSize.x + mAABBOffset.x >= leftWallX) |
4 |
{
|
5 |
mPosition.x = leftWallX + mAABB.halfSize.x - mAABBOffset.x; |
6 |
mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); |
7 |
mPushesLeftWall = true; |
8 |
}
|
Jika salah satu kondisi selain yang terakhir bernilai false, kita perlu mengatur mPushesLeftWall menjadi false. Karena kondisi terakhir bernilai salah tidak berarti karakter tidak mendorong tembok, tapi sebaliknya, itu memberi tahu kita bahwa karakter sudah bertabrakan dengan tembok di frame sebelumnya. Karena ini, lebih baik mengubah mPushesLeftWall
menjadi false hanya jika salah satu dari dua kondisi bernilai false.
1 |
if (mSpeed.x <= 0.0f |
2 |
&& CollidesWithLeftWall(mOldPosition, mPosition, out leftWallX)) |
3 |
{
|
4 |
if (mOldPosition.x - mAABB.HalfSizeX + AABBOffsetX >= leftWallX) |
5 |
{
|
6 |
mPosition.x = leftWallX + mAABB.HalfSizeX - AABBOffsetX; |
7 |
mPushesLeftWall = true; |
8 |
}
|
9 |
mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); |
10 |
}
|
11 |
else
|
12 |
mPushesLeftWall = false; |
Sekarang kita periksa tabrakan dengan tembok di kanan.
1 |
if (mSpeed.x >= 0.0f |
2 |
&& CollidesWithRightWall(mOldPosition, mPosition, out rightWallX)) |
3 |
{
|
4 |
if (mOldPosition.x + mAABB.HalfSizeX + AABBOffsetX <= rightWallX) |
5 |
{
|
6 |
mPosition.x = rightWallX - mAABB.HalfSizeX - AABBOffsetX; |
7 |
mPushesRightWall = true; |
8 |
}
|
9 |
|
10 |
mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); |
11 |
}
|
12 |
else
|
13 |
mPushesRightWall = false; |
Seperti yang bisa kamu lihat, kita menggunakan formula yang sama dengan pemeriksaan tabrakan dengan tembok kiri, tapi berupa hasil pencerminannya.
Kita sudah memiliki kode untuk memeriksa tabrakan dengan tanah, jadi setelah itu kita perlu memeriksa tabrakan dengan langit-langit. Tidak ada yang nbaru di sini, kita pun tidak perlu membuat pemeriksaan tambahan kecuali kecepatan vertikal perlu lebih besar atau sama dengan nol, artinya kita benar-benar bertabrakan dengan petak di atas kita.
1 |
if (mSpeed.y >= 0.0f |
2 |
&& HasCeiling(mOldPosition, mPosition, out ceilingY)) |
3 |
{ |
4 |
mPosition.y = ceilingY - mAABB.halfSize.y - mAABBOffset.y - 1.0f; |
5 |
mSpeed.y = 0.0f; |
6 |
mAtCeiling = true; |
7 |
} |
8 |
else |
9 |
mAtCeiling = false; |
Membulatkan Pojokan Petak
Sebelum kita menguji apakah respon tabrakan berfungsi dengan benar, ada satu hal penting untuk dilakukan, yaitu untuk membulatkan nilai pojok yang kita perhitungkan dalam pemeriksaan tabrakan. Kita perlu melakukannya agar pemeriksaan kita tidak dirusak oleh kesalahan perhitungan bilangan real, yang mungkin muncul dari posisi aneh di peta, skala karakter, atau sekedar ukuran AABB yang aneh.
Pertama, untuk kemudahan kita, kita buat sebuah fungsi untuk mengubah vektor berisi bilangan real, menjadi vektor berisi bilangan yang sudah dibulatkan.
1 |
Vector2 RoundVector(Vector2 v) |
2 |
{
|
3 |
return new Vector2(Mathf.Round(v.x), Mathf.Round(v.y)); |
4 |
}
|
Sekarang kita gunakan fungsi ini untuk setiap pemeriksaan tabrakan. Pertama, kita perbaiki fungsi HasCeiling
.
1 |
var oldTopRight = RoundVector(oldCenter + mAABB.HalfSize + Vector2.up - Vector2.right); |
2 |
|
3 |
var newTopRight = RoundVector(center + mAABB.HalfSize + Vector2.up - Vector2.right); |
4 |
var newTopLeft = RoundVector(new Vector2(newTopRight.x - mAABB.HalfSizeX * 2.0f + 2.0f, newTopRight.y)); |
Berikutnya OnGround
.
1 |
var oldBottomLeft = RoundVector(oldCenter - mAABB.HalfSize - Vector2.up + Vector2.right); |
2 |
|
3 |
var newBottomLeft = RoundVector(center - mAABB.HalfSize - Vector2.up + Vector2.right); |
4 |
var newBottomRight = RoundVector(new Vector2(newBottomLeft.x + mAABB.HalfSizeX * 2.0f - 2.0f, newBottomLeft.y)); |
PushesRightWall
.
1 |
var oldBottomRight = RoundVector(oldCenter + new Vector2(mAABB.HalfSizeX, -mAABB.HalfSizeY) + Vector2.right); |
2 |
|
3 |
var newBottomRight = RoundVector(center + new Vector2(mAABB.HalfSizeX, -mAABB.HalfSizeY) + Vector2.right); |
4 |
var newTopRight = RoundVector(newBottomRight + new Vector2(0.0f, mAABB.HalfSizeY * 2.0f)); |
Dan akhirnya, PushesLeftWall
.
1 |
var oldBottomLeft = RoundVector(oldCenter - mAABB.HalfSize - Vector2.right); |
2 |
|
3 |
var newBottomLeft = RoundVector(center - mAABB.HalfSize - Vector2.right); |
4 |
var newTopLeft = RoundVector(newBottomLeft + new Vector2(0.0f, mAABB.HalfSizeY * 2.0f)); |
Modifikasi-modifikasi tersebut harusnya sudah menyelesaikan masalah kita.
Memeriksa Hasilnya
Modifikasinya sudah cukup. Sekarang kita periksa bagaimana hasil respon tabrakan yang sudah kita buat.



Ringkasan
Sekian untuk bagian ini! Kita sudah membuat fungsi-fungsi untuk deteksi tabrakan dengan tilemap, yang seharusnya sudah cukup bisa diandalkan. Kita tahu kondisi setiap objek saat ini: apakah ada di atas tanah, menyentuh petak di kanan atau kiri, atau menyundul langit-langit. Kita juga mengimplementasi platform satu arah, yang sangat penting di setiap game platformer.
Pada bagian berikutnya, kita akan tambahkan mekanik untuk menangkap pinggiran platform, yang akan menambah pergerakan karakter lebih jauh.