Dasar Physics Platformer 2D, Bagian 7: Bidang Miring
() translation by (you can also view the original English article)



Demo
Demo tersebut menunjukkan hasil akhir implementasi bidang miring. Gunakan WASD untuk menggerakkan karakter. Tombol mouse kanan membuat sebuah petak. Kamu bisa menggunakan roda gulir atau tombol panah untuk memilih petak yang ingin kamu tempatkan. Slider mengubah ukuran karakter pemain.
Demo tersebut dibuat menggunakan Unity 5.5.2f1, dan source code juga kompetibel dengan Unity versi tersebut.
Bidang Miring
Bidang miring menambahkan banyak fleksibilitas pada game, dalam hal interaksi yang bisa terjadi dengan medan dalam permainan dan variasi visual, tapi implementasinya bisa sangat rumit, terutama jika kita ingin mendukung banyak jenis bidang miring.
Seperti pada bagian sebelumnya, kita akan melanjutkan pekerjaan yang kita buat, walaupun kita akan mengerjakan ulang sebagian besar kode yang sudah kita tulis. Yang kita perlukan dari bagian awal adalah pergerakan karakter dan tilemap.
Kamu bisa mengunduh file proyek dari bagian sebelumnya lalu menulis kode bersama tutorial ini.
Perubahan pada Integrasi Pergerakan
Karena membuat bidang miring bekerja cukup sulit, akan lebih baik jika kita bisa membuat beberapa hal lebih mudah. Beberapa waktu yang lalu saya menemukan blog post tentang bagaimana Matt Thorson menangani physics dalam gamenya. Pada dasarnya, tekniknya adalah membuat pergerakkan selalu terjadi dalam interval 1 piksel. Jika sebuah pergerakan pada sebuah frame lebih besar dari satu piksel, vektor pergerakan dibagi jadi banyak gerakan 1 piksel, dan setelah setiap gerakan kita periksa tabrakan dengan tanah.
Ini akan memudahkan kita untuk menemukan rintangan sepanjang garis pergerakan di satu waktu bersamaan, melainkan kita lakukan secara iteratif. Teknik ini membuat implementasi lebih sederhana, tapi meningkatkan jumlah pemeriksaan tabrakan yang dilakukan, jadi mungkin teknik ini tidak cocok untuk game di mana banyak objek yang bergerak, terutama permainan resolusi tinggi di mana pergerakan gerak objek lebih tinggi. Keuntungan lainnya adalah walaupun ada lebih banyak pemeriksaan tabrakan, setiap pemeriksaan akan lebih sederhana karena kita tahu karakter hanya bergerak sejauh satu piksel.
Data Bidang Miring
Kita mulai mendefinisikan data yang kita butuhkan untuk merepresentasikan bidang miring. Pertama, kita perlu peta ketinggian dari bidang miring, yang akan menentukan bentuk bidang miring tersebut. Kita mulai dengan bidang miring 45 derajat.



Kita definisikan juga bentuk lain; bentuk ini akan melambangkan tonjolan pada permukaan tanah.



Tentu saja kita ingin menggunakan variasi dari bidang miring ini, tergantung dari di mana kita menempatkannya. Contohnya, untuk bidang miring 45 derajat yang kita buat, cocok jika ada blok solid di sisi kanannya, jika blok solid ada di sebelah kiri, kita perlu gunakan versi pencerminan dari petak bidang miring tersebut. Kita perlu mencerminkan bidang miring di sumbu X dan Y dan memutarnya 90 derajat untuk bisa mengakses semua variasi dari sebuah bidang miring.
Kita lihat bagaimana perubahan untuk bidang miring 45 derajat.



Seperti yang bisa dilihat, dalam kasus ini kita bisa mendapatkan semua variasi dengan pencerminan. Kita tidak perlu memutar bidang miring 90 derajat, tapi kitalihat bagaimana dengan bidang miring kedua.



Dalam kasus ini, rotasi 90 derajat membuat kita bisa menempatkan bidang miring ini di tembok.
Memperhitungkan Offset
Kita gunakan data yang kita definisikan untuk menghitung offset yang perlu diterapkan pada objek yang tumpang tindih dengan sebuah petak. Offset tersebut akan memiliki informasi tentang:
- berapa banyak objek perlu bergerak atas/bawah/kiri/kanan agar tidak bertabrakan dengan petak
- seberapa banyak objek perlu bergerak agar berada tepat di sebelah atas/bawah/kiri/kanan dari permukaan bidang miring



Bagian hijau pada gambar di atas adalah bagian di mana objek tumpang tindih dengan bagian kosong pada petak, dan kotak kuning menunjukkan area di mana objek tumpang tindih dengan bidang miring.
Sekarang, kita mulai melihat bagaimana menghitung offset untuk kasus nomor 1.
Objek tidak bertabrakan dengan bagian manapun dari bidang miring. Artinya kita tidak perlu menggerakkannya keluar dari tabrakan, jadi bagian pertama dari offset kita diatur menjadi 0.
Untuk bagian kedua dari offset, jika kita ingin bagian bawah objek menyentuh bidan gmiring, kita perlu menggerakkannya 3 piksel ke bawah. Jika kita ingin sisi kanan objek menyentuh bidang miring, kita perlu menggerakkannya 3 piksel ke kanan. Untuk sisi kiri objek menyentuh sisi kanan dari bidang miring, kita perlu menggerakkannya 16 piksel ke kanan. Begitu pula, jika kita ingin bagian atas objek menyentuh bidang miring, kita perlu menggerakkannya 16 piksel ke bawah.
Sekarang, kenapa kita perlu informasi berapa jarak antara sisi objek dan bidang miring? Data ini sangat berguna saat kita ingin sebuah objek menempel pada bidang miring.
Sebagai contoh, misalkan objek bergerak ke kiri pada bidang miring 45 derajat kita. Jika objek tersebut bergerak cukup cepat, objek tersebut akan berada di udara, dan akhirnya akan turun ke bidang miring lain, dan seterusnya. Jika kita ingin tetap berada pada bidan gmiring, setiap bergerak ke kiri, kita perlu mendorongkan ke bawah agar tetap menyentuh bidang miring. Animasi di bawah menunjukkan perbedaan antara fitur menempel pada bidang miring aktif atau tidak.



Kita akan menyimpan banyak sekali data di sini, pada dasarnya, kita ingin menghitung offset untuk setiap tumpang tindih yang bisa terjadi dengan sebuah petak. Ini artinya untuk setiap posisi dan untuk setiap ukuran tumpang tindih, kita punya referensi cepat untuk seberapa banyak kita perlu menggerakkan objek. Perlu diperhatikan kita tidak bisa menyimpan offset akhir karena kita tidak bisa menyimpan offset untuk setiap AABB yang mungkin, tapi mudah untuk mengatur offset jika diketahui tumpang tindih AABB dengan petak bidang miring.
Mendefinisikan Petak
Kita akan mendefinisikan semua data bidang miring pada kelas statik Slopes.
1 |
public static class Slopes |
2 |
{
|
3 |
}
|
Pertama, kita tangani peta ketinggian. Kita definisikan beberapa contoh untuk diproses lain kesempatan.
1 |
public static readonly sbyte[] empty = new sbyte[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; |
2 |
public static readonly sbyte[] full = new sbyte[16] { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; |
3 |
public static readonly sbyte[] slope45 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; |
4 |
public static readonly sbyte[] slopeMid1 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 }; |
Kita tambahkan tipe pengujian petak untuk maising-masing bidang miring yang dibuat.
1 |
public enum TileType |
2 |
{
|
3 |
Empty, |
4 |
Block, |
5 |
OneWay, |
6 |
|
7 |
TestSlopeMid1, |
8 |
TestSlopeMid1FX, |
9 |
TestSlopeMid1FY, |
10 |
TestSlopeMid1FXY, |
11 |
TestSlopeMid1F90, |
12 |
TestSlopeMid1F90X, |
13 |
TestSlopeMid1F90Y, |
14 |
TestSlopeMid1F90XY, |
15 |
|
16 |
TestSlope45, |
17 |
TestSlope45FX, |
18 |
TestSlope45FY, |
19 |
TestSlope45FXY, |
20 |
TestSlope45F90, |
21 |
TestSlope45F90X, |
22 |
TestSlope45F90Y, |
23 |
TestSlope45F90XY, |
24 |
|
25 |
Count
|
26 |
}
|
Kita buat enumerasi lain untuk tipe tabrakan petak. Ini akan berguna untuk menentukan tipe tabrakan yang sama untuk berbagai petak, contohnya bidang miring 45 derajat dengan rumput atau bidang miring 45 derajat dengan batu.
1 |
public enum TileCollisionType |
2 |
{
|
3 |
Empty, |
4 |
Block, |
5 |
OneWay, |
6 |
|
7 |
SlopeMid1, |
8 |
SlopeMid1FX, |
9 |
SlopeMid1FY, |
10 |
SlopeMid1FXY, |
11 |
SlopeMid1F90, |
12 |
SlopeMid1F90X, |
13 |
SlopeMid1F90Y, |
14 |
SlopeMid1F90XY, |
15 |
|
16 |
Slope45, |
17 |
Slope45FX, |
18 |
Slope45FY, |
19 |
Slope45FXY, |
20 |
Slope45F90, |
21 |
Slope45F90X, |
22 |
Slope45F90Y, |
23 |
Slope45F90XY, |
24 |
|
25 |
Count
|
26 |
}
|
Sekarang kita buat array yang akan menyimpan semua peta ketinggian petak. Array ini akan diindeks berdasarkan enumerasi TileCollisionType
.
1 |
public static readonly sbyte[] empty = new sbyte[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; |
2 |
public static readonly sbyte[] full = new sbyte[16] { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; |
3 |
public static readonly sbyte[] slope45 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; |
4 |
public static readonly sbyte[] slopeMid1 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 }; |
5 |
|
6 |
public static sbyte[][] slopesHeights; |
Memproses Bidang Miring
Sebelum kita mulai menghitung offset, kita ingin membuat peta ketinggian kita sepenuhnya menjadi bitmap tabrakan. Ini akan membuat mudah untuk menentukan apakah sebuah AABB bertabrakan dengan sebuah petak dan memungkinkan bentuk petak yang rumit jika itu yang kita butuhkan. Kita buat array untuk bitmap-bitmap tersebut.
1 |
public static readonly sbyte[] empty = new sbyte[16] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; |
2 |
public static readonly sbyte[] full = new sbyte[16] { 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16 }; |
3 |
public static readonly sbyte[] slope45 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; |
4 |
public static readonly sbyte[] slopeMid1 = new sbyte[16] { 1, 2, 3, 4, 5, 6, 7, 8, 8, 7, 6, 5, 4, 3, 2, 1 }; |
5 |
|
6 |
public static sbyte[][] slopesHeights; |
7 |
public static sbyte[][][] slopesExtended; |
Sekarang kita buat fungsi yang akan mengubah peta ketinggian menjadi bitmap.
1 |
public static sbyte[][] Extend(sbyte[] slope) |
2 |
{
|
3 |
sbyte[][] extended = new sbyte[Map.cTileSize][]; |
4 |
|
5 |
for (int x = 0; x < Map.cTileSize; ++x) |
6 |
{
|
7 |
extended[x] = new sbyte[Map.cTileSize]; |
8 |
|
9 |
for (int y = 0; y < Map.cTileSize; ++y) |
10 |
extended[x][y] = System.Convert.ToSByte(y < slope[x]); |
11 |
}
|
12 |
|
13 |
return extended; |
14 |
}
|
Tidak ada yang rumit di sini, jika sebuah posisi pada petak adalah solid, kita atur menjadi 1, jika tidak, kita atur menjadi 0.
Sekarang kita buat fungsi Init
, yang akan melakukan semua penyimpanan data yang kita butuhkan terhadap bidang miring.
1 |
public static void Init() |
2 |
{
|
3 |
}
|
Kita buat array penyimpanannya di sini.
1 |
public static void Init() |
2 |
{ |
3 |
slopesHeights = new sbyte[(int)TileCollisionType.Count][]; |
4 |
slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; |
5 |
} |
Sekarang kita buat setiap tipe tabrakan petak merujuk pada data simpanan yang sesuai.
1 |
for (int i = 0; i < (int)TileCollisionType.Count; ++i) |
2 |
{
|
3 |
switch ((TileCollisionType)i) |
4 |
{
|
5 |
case TileCollisionType.Empty: |
6 |
slopesHeights[i] = empty; |
7 |
slopesExtended[i] = Extend(slopesHeights[i]); |
8 |
break; |
9 |
|
10 |
case TileCollisionType.Full: |
11 |
slopesHeights[i] = full; |
12 |
slopesExtended[i] = Extend(slopesHeights[i]); |
13 |
break; |
14 |
|
15 |
case TileCollisionType.Slope45: |
16 |
slopesHeights[i] = slope45; |
17 |
slopesExtended[i] = Extend(slopesHeights[i]); |
18 |
break; |
19 |
case TileCollisionType.Slope45FX: |
20 |
case TileCollisionType.Slope45FY: |
21 |
case TileCollisionType.Slope45FXY: |
22 |
case TileCollisionType.Slope45F90: |
23 |
case TileCollisionType.Slope45F90X: |
24 |
case TileCollisionType.Slope45F90XY: |
25 |
case TileCollisionType.Slope45F90Y: |
26 |
slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45]; |
27 |
slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45]; |
28 |
break; |
29 |
|
30 |
case TileCollisionType.SlopeMid1: |
31 |
slopesHeights[i] = slopeMid1; |
32 |
slopesExtended[i] = Extend(slopesHeights[i]); |
33 |
break; |
34 |
case TileCollisionType.SlopeMid1FX: |
35 |
case TileCollisionType.SlopeMid1FY: |
36 |
case TileCollisionType.SlopeMid1FXY: |
37 |
case TileCollisionType.SlopeMid1F90: |
38 |
case TileCollisionType.SlopeMid1F90X: |
39 |
case TileCollisionType.SlopeMid1F90XY: |
40 |
case TileCollisionType.SlopeMid1F90Y: |
41 |
slopesHeights[i] = slopesHeights[(int)TileCollisionType.SlopeMid1]; |
42 |
slopesExtended[i] = slopesExtended[(int)TileCollisionType.SlopeMid1]; |
43 |
break; |
44 |
}
|
45 |
}
|
Struktur Offset
Sekarang kita bisa mendefinisikan struktur offset kita.
1 |
public struct SlopeOffsetSB |
2 |
{
|
3 |
public sbyte freeLeft, freeRight, freeDown, freeUp, collidingLeft, collidingRight, collidingBottom, collidingTop; |
4 |
|
5 |
public SlopeOffsetSB(sbyte _freeLeft, sbyte _freeRight, sbyte _freeDown, sbyte _freeUp, sbyte _collidingLeft, sbyte _collidingRight, sbyte _collidingBottom, sbyte _collidingTop) |
6 |
{
|
7 |
freeLeft = _freeLeft; |
8 |
freeRight = _freeRight; |
9 |
freeDown = _freeDown; |
10 |
freeUp = _freeUp; |
11 |
|
12 |
collidingLeft = _collidingLeft; |
13 |
collidingRight = _collidingRight; |
14 |
collidingBottom = _collidingBottom; |
15 |
collidingTop = _collidingTop; |
16 |
}
|
17 |
}
|
Seperti dijelaskan sebelumnya, variabel freeLeft
, freeRight
, freeDown
, dan freeUp
merujuk pada offset yang perlu diterapkan agar objek tidak bertabrakan dengan bidang miring, sedangkan collidingLeft
, collidingRight
, collidingTop
, dan collidingBottom
adalah jarak objek perlu digeser untuk menyentuh bidang miring tanpa tumpang tindih.
Saatnya untuk membuat fungsi penyimpanan yang rumit, tapi sebelumnya, kita buat tempat penyimpanan yang akan menyimpan semua data.
1 |
public static sbyte[][] slopesHeights; |
2 |
public static sbyte[][][] slopesExtended; |
3 |
public static SlopeOffsetSB[][][][][] slopeOffsets; |
Dan buat array pada fungsi Init
.
1 |
slopesHeights = new sbyte[(int)TileCollisionType.Count][]; |
2 |
slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; |
3 |
slopeOffsets = new SlopeOffsetSB[(int)TileCollisionType.Count][][][][]; |
Masalah Memory
Seperti bisa kamu lihat, array ini memiliki banyak dimensi, dan masing-masing itpe petak baru akan membutuhkan banyak memory. Untuk setiap posisi X pada petak, untuk setiap posisi Y pada petak, untuk setiap lebar yang mungkin pada petak, dan untuk setiap tinggi yang mungkin, akan ada perhitungan nilai offset yang berbeda.
Karena petak yang kita gunakan adalah 16x16, artinya data yang diperlukan untuk setiap tipe petak adalah 16*16*16*16*8 byte, yang sama dengan 512 kB. Ini adalah data yang sangat banyak, tapi masih bisa dikelola, dan jika menyimpan banyak informasi tidak memungkinkan, kita perlu beralih ke menghitung offset secara real time, misalnya menggunakan metode yang lebih efisien dibanding yang kita gunakan untuk penyimpanan data, atau mengoptimasi data kita.
Sekarang, jika ukuran petak dalam game kita lebih besar, misalnya 32x32, setiap petak akan membutuhkan 8 MB, dan jika kita gunakan 64x64, ukurannya menjadi 128MB. Ukuran ini terlihat terlalu besar, terutama jika kita ingin menggunakan beberapa jenis bidang miring dalam game. Solusi yang masuk akal adalah membagi petak yang besar menjadi lebih kecil. Ingatlah bahwa hanya bidang miring baru didefinisikan yang membutuhkan ruang penyimpanan, transformasi akan menggunakan data yang sudah ada.
Memeriksa tabrakan di dalam petak
Sebelum kita mulai menghitung offset, kita perlu tahu jika objek di suatu posisi akan bertabrakan dengan bagian solid dari petak. Kita buat fungsi ini terlebih dahulu.
1 |
public static bool Collides(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) |
2 |
{
|
3 |
for (int x = posX; x <= posX + w && x < Map.cTileSize; ++x) |
4 |
{
|
5 |
for (int y = posY; y <= posY + h && y < Map.cTileSize; ++y) |
6 |
{
|
7 |
if (slopeExtended[x][y] == 1) |
8 |
return true; |
9 |
}
|
10 |
}
|
11 |
|
12 |
return false; |
13 |
}
|
Fungsi ini menerima bitmap collision, posisi tumpang tindih, dan ukuran tumpang tindih. Posisi adalah piksel kiri bawah pada objek, dan ukuran adalah lebar dan tinggi objek berbasis 0. Berbasis 0 maksudnya jika bernilai 0 artinya lebar objek sebenarnya 1 piksel, dan lebar 15 artinya lebar objek 16 piksel. Fungsi ini sangat sederhana, jika ada piksel objek yang tumpang tindih dengan bidang miring, kita kembalikan true, jika tidak kita kembalikan false.
Menghitung Offset
Sekarang kita mulai hitung offset.
1 |
public static SlopeOffsetSB GetOffset(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) |
2 |
{
|
3 |
}
|
Sekali lagi, untuk menghitung offset kita perlu bitmap collision, posisi, dan ukuran tumpang tindih. Kita mulai dengan deklarasi nilai offset.
1 |
public static SlopeOffsetSB GetOffset(sbyte[][] slopeExtended, sbyte posX, sbyte posY, sbyte w, sbyte h) |
2 |
{
|
3 |
sbyte freeUp = 0, freeDown = 0, collidingTop = 0, collidingBottom = 0; |
4 |
sbyte freeLeft = 0, freeRight = 0, collidingLeft = 0, collidingRight = 0; |
5 |
}
|
Sekarang kita hitung seberapa banyak kita perlu menggeser objek agar tidak bertabrakan dengan bidang miring. Untuk itu, selama objek bertabrakan dengan bidang miring, kita perlu terus menggesernya ke atas dan memeriksa tabrakan sampai tidak ada tumpang tindih dengan bagian solid dari petak.



Gambar di atas adalah ilustrasi bagaimana kita menghitung offset. Pada kasus pertama, karena objek menyentuh bagian atas dari petak, bukan hanya kita geser ke atas, kita perlu kurangi tingginya. Itu karena jika ada bagian dari AABB bergerak ke luar batas petak, kita tidak tertarik lagi dengannya. Begitu pula, offset dihitung untuk semua arah, jadi pada contoh di tas, offsetnya:
- 4 untuk offset atas
- -4 untuk offset kiri
- -16 untuk offset bawah, yang merupakan jarak maksimum karena jika kita gerakkan objek ke bawah, kita perlu menggerakkannya sampai batas untuk menghentikan tabrakan dengan bidang miring
- 16 untuk offset kanan
Kita mulai dengan deklarasi variabel sementara untuk tinggi objek. Seperti disebutkan di atas, ini akan berubah tergantung seberapa tinggi kita menggerakkan objek.
1 |
sbyte movH = h; |
Sekarang untuk kondisi utama. Selama objek belum keluar dari batas petak dan bertabrakan dengan bagian solid dari petak, kita perlu meningkatkan offsetUp
.
1 |
sbyte movH = h; |
2 |
while (movH >= 0 && posY + freeUp < Map.cTileSize && Collides(slopeExtended, posX, (sbyte)(posY + freeUp), w, movH)) |
3 |
{
|
4 |
++freeUp; |
5 |
}
|
Lalu, atur ukuran area tumpang tindih petak-objek jika objek bergerak keluar batas petak.
1 |
sbyte movH = h; |
2 |
while (movH >= 0 && posY + freeUp < Map.cTileSize && Collides(slopeExtended, posX, (sbyte)(posY + freeUp), w, movH)) |
3 |
{
|
4 |
if (posY + freeUp == Map.cTileSize) |
5 |
--movH; |
6 |
|
7 |
++freeUp; |
8 |
}
|
Sekarang kita lakukan hal yang sama untuk offset kiri. Ingatlah bahwa saat kita menggerakkan objek ke kiri dan objek sedang bergerak keluar batas petak, kita tidak perlu mengubah posisinya, melainkan kita ubah lebar dari tumpang tindih. Hal ini digambarkan di sisi kanan animasi perhitungan offset.
1 |
movW = w; |
2 |
while (movW >= 0 && posX + freeLeft >= 0 && Collides(slopeExtended, (sbyte)(posX + freeLeft), posY, movW, h)) |
3 |
{
|
4 |
if (posX + freeLeft == 0) |
5 |
--movW; |
6 |
else
|
7 |
--freeLeft; |
8 |
}
|
Di sini, karena saat mengurangi lebar kita tidak menggerakkan offset freeLeft
, kita perlu mengubah nilai yang berkurang menjadi offset.
1 |
movW = w; |
2 |
while (movW >= 0 && posX + freeLeft >= 0 && Collides(slopeExtended, (sbyte)(posX + freeLeft), posY, movW, h)) |
3 |
{
|
4 |
if (posX + freeLeft == 0) |
5 |
--movW; |
6 |
else
|
7 |
--freeLeft; |
8 |
}
|
9 |
freeLeft -= (sbyte)(w - movW); |
Sekarang lakukan hal yang sama untuk offset bawah dan kanan.
1 |
movH = h; |
2 |
while (movH >= 0 && posY + freeDown >= 0 && Collides(slopeExtended, posX, (sbyte)(posY + freeDown), w, movH)) |
3 |
{
|
4 |
if (posY + freeDown == 0) |
5 |
--movH; |
6 |
else
|
7 |
--freeDown; |
8 |
}
|
9 |
|
10 |
freeDown -= (sbyte)(h - movH); |
11 |
|
12 |
sbyte movW = w; |
13 |
while (movW >= 0 && posY + freeRight < Map.cTileSize && Collides(slopeExtended, (sbyte)(posX + freeRight), posY, movW, h)) |
14 |
{
|
15 |
if (posX + freeRight == Map.cTileSize) |
16 |
--movW; |
17 |
|
18 |
++freeRight; |
19 |
}
|
Kita sudah menghitung bagian pertama dari offset, yaitu seberapa jauh kita perlu menggeser objek untuk menghentikannya tabrakan dengan bidang miring. Sekarang waktunya menghitung offset untuk menggeser objek tepat ke sebelah bagian solid dari petak.
Perhatikan jika kita perlu menggeser objek keluar dari tabrakan, kita sudah melakukannya, karena kita berhenti tepat ketika tidak terjadi tabrakan.



Pada kasus di kanan, offset atas bernilai 4, tapi itu juga nilai offset yang kita perlukan untuk menggeser objek agar bagian bawahnya berada pada piksel solid. Hal yang sama terjadi pada sisi satunya.
1 |
if (freeUp == 0) |
2 |
{
|
3 |
|
4 |
}
|
5 |
else
|
6 |
collidingBottom = freeUp; |
Kita perlu menghitung offset pada kasus di kiri. Jika kita ingin menemukan offset collidingBottom
, kita perlu menggeser objek 3 piksel ke bawah. Perhitungan yang dibutuhkan di sini sama dengan sebelumnya, tapi kali ini kita mencari saat objek tabrakan dengan bidang miring, lalu menggesernya sembari mengurangi offset dengan satu, jadi objek menyentuh piksel solid, bukan menimpanya.
1 |
if (freeUp == 0) |
2 |
{
|
3 |
while ( posY + collidingBottom >= 0 && !Collides(slopeExtended, posX, (sbyte)(posY + collidingBottom), w, h)) |
4 |
--collidingBottom; |
5 |
|
6 |
collidingBottom += 1; |
7 |
}
|
8 |
else
|
9 |
{
|
10 |
collidingBottom = freeUp; |
11 |
}
|
Jika freeUp
bernilai 0, freeDown pasti bernilai 0 juga, jadi kita bisa memasukkan perhitungan collidingTop
pada blok kode yang sama. Perhitungan ini mirip dengan yang sudah kita buat sejauh ini.
1 |
if (freeUp == 0) |
2 |
{
|
3 |
while (posY + h + collidingTop < Map.cTileSize && !Collides(slopeExtended, posX, (sbyte)(posY + collidingTop), w, h)) |
4 |
++collidingTop; |
5 |
|
6 |
collidingTop -= 1; |
7 |
|
8 |
while ( posY + collidingBottom >= 0 && !Collides(slopeExtended, posX, (sbyte)(posY + collidingBottom), w, h)) |
9 |
--collidingBottom; |
10 |
|
11 |
collidingBottom += 1; |
12 |
}
|
13 |
else
|
14 |
{
|
15 |
collidingBottom = freeUp; |
16 |
collidingTop = freeDown; |
17 |
}
|
Kita lakukan hal yang sama untuk offset kiri dan kanan.
1 |
if (freeRight == 0) |
2 |
{
|
3 |
while (posX + w + collidingRight < Map.cTileSize && !Collides(slopeExtended, (sbyte)(posX + collidingRight), posY, w, h)) |
4 |
++collidingRight; |
5 |
|
6 |
collidingRight -= 1; |
7 |
|
8 |
while (posX + collidingLeft >= 0 && !Collides(slopeExtended, (sbyte)(posX + collidingLeft), posY , w, h)) |
9 |
--collidingLeft; |
10 |
|
11 |
collidingLeft += 1; |
12 |
}
|
13 |
else
|
14 |
{
|
15 |
collidingLeft = freeRight; |
16 |
collidingRight = freeLeft; |
17 |
}
|
Menyimpan data
Sekarang semua offset sudah diperhitungkan, kita bisa mengembalikan offset untuk set data ini.
1 |
return new SlopeOffsetSB(freeLeft, freeRight, freeDown, freeUp, collidingLeft, collidingRight, collidingBottom, collidingTop); |
Kita buat tempat penyimpanan untuk data kita.
1 |
public static sbyte[][] slopesHeights; |
2 |
public static sbyte[][][] slopesExtended; |
3 |
public static SlopeOffsetSB[][][][][] slopeOffsets; |
Menginisialisasi array
1 |
slopesHeights = new sbyte[(int)TileCollisionType.Count][]; |
2 |
slopesExtended = new sbyte[(int)TileCollisionType.Count][][]; |
3 |
slopeOffsets = new SlopeOffsetSB[(int)TileCollisionType.Count][][][][]; |
Dan akhirnya, buat fungsi penyimpanan data.
1 |
public static SlopeOffsetSB[][][][] CacheSlopeOffsets(sbyte[][] slopeExtended) |
2 |
{
|
3 |
var offsetCache = new SlopeOffsetSB[Map.cTileSize][][][]; |
4 |
|
5 |
for (int x = 0; x < Map.cTileSize; ++x) |
6 |
{
|
7 |
offsetCache[x] = new SlopeOffsetSB[Map.cTileSize][][]; |
8 |
|
9 |
for (int y = 0; y < Map.cTileSize; ++y) |
10 |
{
|
11 |
offsetCache[x][y] = new SlopeOffsetSB[Map.cTileSize][]; |
12 |
|
13 |
for (int w = 0; w < Map.cTileSize; ++w) |
14 |
{
|
15 |
offsetCache[x][y][w] = new SlopeOffsetSB[Map.cTileSize]; |
16 |
|
17 |
for (int h = 0; h < Map.cTileSize; ++h) |
18 |
{
|
19 |
offsetCache[x][y][w][h] = GetOffset(slopeExtended, (sbyte)x, (sbyte)y, (sbyte)w, (sbyte)h); |
20 |
}
|
21 |
}
|
22 |
}
|
23 |
}
|
24 |
|
25 |
return offsetCache; |
26 |
}
|
Fungsi tersebut sangat sederhana, jadi cukup mudah untuk melihat bagaimana data sebanyak itu disimpan sesuai dengan untuk kebutuhan kita.
Sekarang pastikan untuk menyimpan semua offset untuk setiap jenis petak collision.
1 |
case TileCollisionType.Slope45: |
2 |
slopesHeights[i] = slope45; |
3 |
slopesExtended[i] = Extend(slopesHeights[i]); |
4 |
slopeOffsets[i] = CacheSlopeOffsets(slopesExtended[i]); |
5 |
break; |
6 |
case TileCollisionType.Slope45FX: |
7 |
case TileCollisionType.Slope45FY: |
8 |
case TileCollisionType.Slope45FXY: |
9 |
case TileCollisionType.Slope45F90: |
10 |
case TileCollisionType.Slope45F90X: |
11 |
case TileCollisionType.Slope45F90XY: |
12 |
case TileCollisionType.Slope45F90Y: |
13 |
slopesHeights[i] = slopesHeights[(int)TileCollisionType.Slope45]; |
14 |
slopesExtended[i] = slopesExtended[(int)TileCollisionType.Slope45]; |
15 |
slopeOffsets[i] = slopeOffsets[(int)TileCollisionType.Slope45]; |
16 |
break; |
Dan dengan begitu fungsi penyimpanan data kita selesai.
Menghitung offset ruang dunia (World Space)
Sekarang kita gunakan data yang sudah kita simpan untuk membuat fungsi yang akan mengembalikan offset untuk karakter yang berada pada ruang dunia pemainan.
1 |
public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) |
2 |
{
|
3 |
}
|
Offset yang akan kita kembalikan tidak sama dengan struktur yang kita gunakan pada data yang disimpan, karena offset ruang dunia bisa lebih besar dari batasan satu byte. Struktur ini kurang lebih sama, tapi menggunakan integer.
Parameternya adalah sebagai berikut:
- posisi titik tengah petak dalam ruang dunia
- sisi kiri, kanan, bawah, dan atas dari AABB yang kita cari nilai offsetnya
- jenis perak yang ingin kita cari offsetnya
Pertama, kita perlu mencari tahu bagaimana AABB tumpang tindih dengan petak bidang miring. Kita perlu tahu di mana tumpang tindih dimulai (pojok kiri bawah), dan sebesar apa tumpang tindih pada petak tersebut.
Untuk menghitung ini, pertama kita deklarasi variabel yang dibutuhkan.
1 |
public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) |
2 |
{
|
3 |
int posX, posY, sizeX, sizeY; |
4 |
SlopeOffsetI offset; |
5 |
}
|
Sekarang kita hitung sisi-sisi pada petak dalam ruang dunia.
1 |
float leftTileEdge = tileCenter.x - Map.cTileSize / 2; |
2 |
float rightTileEdge = leftTileEdge + Map.cTileSize; |
3 |
float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; |
4 |
float topTileEdge = bottomTileEdge + Map.cTileSize; |
Bagian ini akan cukup mudah. Ada dua kategori utama kasus yang bisa kita temukan. Pertama adalah tumpang tindih di dalam batasan petak.



Piksel biru gelap adalah posisi tumpang tindih, lalu tinggi dan lebar ditandai dengan petak biru. Berikutnya akan cukup sederhana, menghitung posisi dan ukuran tumpang tindih tidak perlu aksi tambahan.
Kasus kategori kedua adalah seperti berikut, dan dalam game kita akan lebih banyak menemui kasus seperti ini:



Kita lihat contoh pada gambar di atas. Seperti yang kamu bisa lihat, AABB menembus batas petak, tapi yang perlu kita cari tahu adalahah posisi dan ukuran tumpang tindih di dalam petak tersebut, agar kita bisa mendapat nilai offset yang kita simpan. Sekarang kita tidak peduli tentang apapun di luar batas petak. Hal ini memerlukan kita untuk membatasi posisi tumpang tindih dan ukurannya ke batas petak.
Posisi x sama dengan offset antara sisi kiri AABB dan sisi kiri petak. Jika AABB ada di kiri batas kisi petak, posisi perlu dibatasi menjadi 0. Untuk mendapatkan lebar tumpang tindih, kita perlu mengurangi riri kanan AABB dari posisi x tumpang tindih, yang sudah kita hitung sebelumnya.
Nilai sumbu Y dihitung dengan cara yang sama.
1 |
posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); |
2 |
sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); |
3 |
|
4 |
posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); |
5 |
sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); |
Sekarang kita bisa mengambil offset yang sudah disimpan untuk tumpang tindih tersebut.
1 |
offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); |
Mengatur Offset
Sebelum kita kembalikan offset, kita mungkin perlu mengaturnya. Pertimbangkan situasi berikut.



Kita lihat bagaimana offset yang kita simpan untuk tumpang tindih tersebut terlihat. Saat menyimpan data, kita hanya memedulikan tumpang tindih di dalam batas petak, dalam kasus ini, offset akan bernilai 9. Kamu bisa lihat jika kita menggeser area tumpang tindih di dalam petak 9 piksel ke atas, objek akan berhenti tabrakan dengan bidang miring, tapi jika kita geser keseluruhan AABB, maka area di bawah batas petak akan masuk ke dalam tabrakan.
Pada dasarnya, kita perlu mengatur offset atas sejumlah piksel AABB di bawah batas petak.
1 |
if (bottomTileEdge > bottomY) |
2 |
{
|
3 |
if (offset.freeUp > 0) |
4 |
offset.freeUp += (int)bottomTileEdge - (int)bottomY; |
5 |
offset.collidingBottom = offset.freeUp; |
6 |
}
|
Hal yang sama perlu dilakukan untuk offset lainnya, kiri, kanan, dan bawah, kecuali saat ini kita lewati penanganan offset kiri dan kanan karena tidak diperlukan saat ini.
1 |
if (topTileEdge < topY) |
2 |
{
|
3 |
if (offset.freeDown < 0) |
4 |
offset.freeDown -= (int)(topY - topTileEdge); |
5 |
offset.collidingTop = offset.freeDown; |
6 |
}
|
7 |
if (bottomTileEdge > bottomY) |
8 |
{
|
9 |
if (offset.freeUp > 0) |
10 |
offset.freeUp += (int)bottomTileEdge - (int)bottomY; |
11 |
offset.collidingBottom = offset.freeUp; |
12 |
}
|
Begitu kita selesai, kita bisa mengembalikan offset yang sudah diatur. Fungsi yang sudah selesai akan terlihat seperti ini.
1 |
public static SlopeOffsetI GetOffset(Vector2 tileCenter, float leftX, float rightX, float bottomY, float topY, TileCollisionType tileCollisionType) |
2 |
{
|
3 |
int posX, posY, sizeX, sizeY; |
4 |
|
5 |
float leftTileEdge = tileCenter.x - Map.cTileSize / 2; |
6 |
float rightTileEdge = leftTileEdge + Map.cTileSize; |
7 |
float bottomTileEdge = tileCenter.y - Map.cTileSize / 2; |
8 |
float topTileEdge = bottomTileEdge + Map.cTileSize; |
9 |
SlopeOffsetI offset; |
10 |
|
11 |
posX = (int)Mathf.Clamp(leftX - leftTileEdge, 0.0f, Map.cTileSize - 1); |
12 |
sizeX = (int)Mathf.Clamp(rightX - (leftTileEdge + posX), 0.0f, Map.cTileSize - 1); |
13 |
|
14 |
posY = (int)Mathf.Clamp(bottomY - bottomTileEdge, 0.0f, Map.cTileSize - 1); |
15 |
sizeY = (int)Mathf.Clamp(topY - (bottomTileEdge + posY), 0.0f, Map.cTileSize - 1); |
16 |
|
17 |
offset = new SlopeOffsetI(slopeOffsets[(int)tileCollisionType][posX][posY][sizeX][sizeY]); |
18 |
|
19 |
if (topTileEdge < topY) |
20 |
{
|
21 |
if (offset.freeDown < 0) |
22 |
offset.freeDown -= (int)(topY - topTileEdge); |
23 |
offset.collidingTop = offset.freeDown; |
24 |
}
|
25 |
if (bottomTileEdge > bottomY) |
26 |
{
|
27 |
if (offset.freeUp > 0) |
28 |
offset.freeUp += Mathf.RoundToInt(bottomTileEdge - bottomY); |
29 |
offset.collidingBottom = offset.freeUp; |
30 |
}
|
31 |
|
32 |
return offset; |
33 |
}
|
Tentu saja, fungsi tersebut tidak sepenuhnya selesai. Nanti kita erlu untuk menangani transformasi petak di sini, jadi offset yang dikembalikan tergantung apakah petak tersebut sudah dicerminkan pada sumbu XY atau diputar 90 derajat. Kali ini, kita hanya menggunakan petak yang tidak ditransformasi.
Mengimplementasi Langkah Physics satu piksel.
Gambaran Umum
Menggerakkan objek satu piksel akan memudahkan kita untuk menangani berbagai hal, terutama tabrakan terhadap bidang miring untuk objek yang cepat. Walaupun kita akan memeriksa tabrakan untuk setiap piksel gerakan kita, kita perlu bergerak dengan pola tertentu untuk memastikan akurasi. Pola ini tidak akan tergantung pada kecepatan objek,



Pada gambar di atas, kamu bisa lihat jika kita gerakkan objek sejumlah piksel yang dibutuhkan secara horizontal lalu vetikal, panah bisa berakhir menabrak dengan tanah yang tidak benar-benar berada pada jalur geraknya. Urutan pergerakkan harus berdasarkan rasio kecepatan vertikal terhadap horizontal; dengan begini kita tahu berapa banyak pikel kita perlu menggeser objek vertikal untuk setiap piksel objek bergerak horizontal.
Definisikan datanya
Kita lanjut ke kelas objek bergerak dan mendefinisikan beberapa variabel baru.
Pertama, variabel mPosition
akan hanya menyimpan bilangan bulat, dan kita akan menyimpan variabel bernama mRemainder
untuk menyimpan nilai di belakang koma.
1 |
public Vector2 mPosition; |
2 |
public Vector2 mRemainder; |
Berikutnya, kita tambahkan beberapa variabel status posisi untuk menunjukkan apakah karakter berada pada bidang miring. Di titik ini, lebih baik jika kita mengelompokkan semua status posisi dalam sebuah struktur.
1 |
[Serializable] |
2 |
public struct PositionState |
3 |
{
|
4 |
public bool pushesRight; |
5 |
public bool pushesLeft; |
6 |
public bool pushesBottom; |
7 |
public bool pushesTop; |
8 |
|
9 |
public bool pushedTop; |
10 |
public bool pushedBottom; |
11 |
public bool pushedRight; |
12 |
public bool pushedLeft; |
13 |
|
14 |
public bool pushedLeftObject; |
15 |
public bool pushedRightObject; |
16 |
public bool pushedBottomObject; |
17 |
public bool pushedTopObject; |
18 |
|
19 |
public bool pushesLeftObject; |
20 |
public bool pushesRightObject; |
21 |
public bool pushesBottomObject; |
22 |
public bool pushesTopObject; |
23 |
|
24 |
public bool pushedLeftTile; |
25 |
public bool pushedRightTile; |
26 |
public bool pushedBottomTile; |
27 |
public bool pushedTopTile; |
28 |
|
29 |
public bool pushesLeftTile; |
30 |
public bool pushesRightTile; |
31 |
public bool pushesBottomTile; |
32 |
public bool pushesTopTile; |
33 |
|
34 |
public bool onOneWayPlatform; |
35 |
|
36 |
public Vector2i leftTile; |
37 |
public Vector2i rightTile; |
38 |
public Vector2i topTile; |
39 |
public Vector2i bottomTile; |
40 |
|
41 |
public void Reset() |
42 |
{
|
43 |
leftTile = rightTile = topTile = bottomTile = new Vector2i(-1, -1); |
44 |
|
45 |
pushesRight = false; |
46 |
pushesLeft = false; |
47 |
pushesBottom = false; |
48 |
pushesTop = false; |
49 |
|
50 |
pushedTop = false; |
51 |
pushedBottom = false; |
52 |
pushedRight = false; |
53 |
pushedLeft = false; |
54 |
|
55 |
pushedLeftObject = false; |
56 |
pushedRightObject = false; |
57 |
pushedBottomObject = false; |
58 |
pushedTopObject = false; |
59 |
|
60 |
pushesLeftObject = false; |
61 |
pushesRightObject = false; |
62 |
pushesBottomObject = false; |
63 |
pushesTopObject = false; |
64 |
|
65 |
pushedLeftTile = false; |
66 |
pushedRightTile = false; |
67 |
pushedBottomTile = false; |
68 |
pushedTopTile = false; |
69 |
|
70 |
pushesLeftTile = false; |
71 |
pushesRightTile = false; |
72 |
pushesBottomTile = false; |
73 |
pushesTopTile = false; |
74 |
|
75 |
onOneWayPlatform = false; |
76 |
}
|
77 |
}
|
Sekarang deklarasikan instans dari struktur tersebut.
1 |
public PositionState mPS; |
Variabel berikutnya yang akan kita butuhkan adalah status menempel pada bidang miring.
1 |
public bool mSticksToSlope; |
Implementasi dasar
Kita mulai dengan membuat fungsi pengecekan tabrakan dasar; saat ini belum menangani bidang miring.
Pemeriksaan Tabrakan
Kita mulai dengan sisi kanan.
1 |
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) |
2 |
{
|
3 |
}
|
Parameter yang digunakan di sini adalah posisi objek saat ini, pojok kanan atas dan kiri bawah, dan status posisinya. Pertama kita perhitungkan pojok kanan atas dan kiri bawah petak untuk objek kita.
1 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); |
2 |
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
Sekarang kita iterasi semua petak pada sisi kanan objek.
1 |
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) |
2 |
{
|
3 |
var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); |
4 |
}
|
Lalu, tergantung dari petak collision, kita atur langkah berikutnya.
1 |
switch (tileCollisionType) |
2 |
{
|
3 |
default://slope |
4 |
break; |
5 |
case TileCollisionType.Empty: |
6 |
break; |
7 |
case TileCollisionType.Full: |
8 |
state.pushesRightTile = true; |
9 |
state.rightTile = new Vector2i(topRightTile.x, y); |
10 |
return true; |
11 |
}
|
Seperti yang bisa kamu lihat, saat ini kita melewati menangani bidang miring, kita hanya ingin menyelesaikan pengaturan dasar sebelum kita masuk ke bidang miring.
Keseluruhan, fungsi kita seharusnya terlihat seperti ini:
1 |
public bool CollidesWithTileRight(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x + 0.5f, topRight.y - 0.5f)); |
4 |
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
5 |
|
6 |
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) |
7 |
{
|
8 |
var tileCollisionType = mMap.GetCollisionType(topRightTile.x, y); |
9 |
|
10 |
switch (tileCollisionType) |
11 |
{
|
12 |
default://slope |
13 |
break; |
14 |
case TileCollisionType.Empty: |
15 |
break; |
16 |
case TileCollisionType.Full: |
17 |
state.pushesRightTile = true; |
18 |
state.rightTile = new Vector2i(topRightTile.x, y); |
19 |
return true; |
20 |
}
|
21 |
}
|
22 |
|
23 |
return false; |
24 |
}
|
Kita lakukan yang sama untuk tiga arah lainnya: kiri, atas, dan bawah.
1 |
public bool CollidesWithTileLeft(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) |
2 |
{
|
3 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); |
4 |
Vector2i bottomLeftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x - 0.5f, bottomLeft.y + 0.5f)); |
5 |
|
6 |
for (int y = bottomLeftTile.y; y <= topRightTile.y; ++y) |
7 |
{
|
8 |
var tileCollisionType = mMap.GetCollisionType(bottomLeftTile.x, y); |
9 |
|
10 |
switch (tileCollisionType) |
11 |
{
|
12 |
default://slope |
13 |
break; |
14 |
case TileCollisionType.Empty: |
15 |
break; |
16 |
case TileCollisionType.Full: |
17 |
state.pushesLeftTile = true; |
18 |
state.leftTile = new Vector2i(bottomLeftTile.x, y); |
19 |
return true; |
20 |
}
|
21 |
}
|
22 |
|
23 |
return false; |
24 |
}
|
25 |
|
26 |
public bool CollidesWithTileTop(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) |
27 |
{
|
28 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y + 0.5f)); |
29 |
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y + 0.5f)); |
30 |
|
31 |
for (int x = bottomleftTile.x; x <= topRightTile.x; ++x) |
32 |
{
|
33 |
var tileCollisionType = mMap.GetCollisionType(x, topRightTile.y); |
34 |
|
35 |
switch (tileCollisionType) |
36 |
{
|
37 |
default://slope |
38 |
break; |
39 |
case TileCollisionType.Empty: |
40 |
break; |
41 |
case TileCollisionType.Full: |
42 |
state.pushesTopTile = true; |
43 |
state.topTile = new Vector2i(x, topRightTile.y); |
44 |
return true; |
45 |
}
|
46 |
}
|
47 |
|
48 |
return false; |
49 |
}
|
50 |
|
51 |
public bool CollidesWithTileBottom(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState stat) |
52 |
{
|
53 |
Vector2i topRightTile = mMap.GetMapTileAtPoint(new Vector2(topRight.x - 0.5f, topRight.y - 0.5f)); |
54 |
Vector2i bottomleftTile = mMap.GetMapTileAtPoint(new Vector2(bottomLeft.x + 0.5f, bottomLeft.y - 0.5f)); |
55 |
|
56 |
for (int x = bottomleftTile.x; x <= topRightTile.x; ++x) |
57 |
{
|
58 |
var tileCollisionType = mMap.GetCollisionType(x, bottomleftTile.y); |
59 |
|
60 |
switch (tileCollisionType) |
61 |
{
|
62 |
default://slope |
63 |
break; |
64 |
case TileCollisionType.Empty: |
65 |
break; |
66 |
case TileCollisionType.Full: |
67 |
state.onOneWayPlatform = false; |
68 |
state.pushesBottomTile = true; |
69 |
state.bottomTile = new Vector2i(x, bottomleftTile.y); |
70 |
return true; |
71 |
}
|
72 |
}
|
73 |
|
74 |
return false; |
75 |
}
|
Fungsi pergerakan
Setelah menangani hal tersebut, kita bisa mulai membuat dua fungsi untuk pergerakan objek. Satu akan menangani gerakan horizontal, dan fungsi lainnya akan menangani pergerakan vertikal.
1 |
public void MoveX(ref Vector2 position, ref bool foundObstacleX, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) |
2 |
{
|
3 |
}
|
Parameter yang akan kita gunakan pada fungsi ini adalah posisi saat ini, boolean yang menunjukkan apakah kita menemukan rintangan sepanjang jalan atau tidak, sebuah offset yang menunjukkan seberapa banyak kira perlu bergerak, step yaitu jarak objek harus digerakkan setiap iterasi, titik kiri bawah dan kanan atas dari AABB, dan status posisi.
Pada dasarnya, yang kita ingin lakukan adalah menggerakkan objek sejauh step beberapa kali,sampai akhirnya total step mencapai offset. Tentu saja jika kita menemui rintangan, kita perlu berhenti bergerak.
1 |
while (!foundObstacleX && offset != 0.0f) |
2 |
{
|
3 |
}
|
Pada setiap iterasi, kita kurangi step dari offset, jadi offset akhirnya menjadi nol, dan kita tahu kita bergerak sesuai dengan jumlah piksel yang seharusnya.
1 |
while (!foundObstacleX && offset != 0.0f) |
2 |
{
|
3 |
offset -= step; |
4 |
}
|
Di setiap step, kita ingin memeriksa apakah kita bertabrakan dengan sebuah petak. Jika kita bergerak ke kanan, kita ingin memeriksa apakah kita bertabrakan dengan tembok di kanan, jika kita bergerak ke kiri, kita ingin memeriksa rintangan di sebelah kiri.
1 |
while (!foundObstacleX && offset != 0.0f) |
2 |
{
|
3 |
offset -= step; |
4 |
|
5 |
if (step > 0.0f) |
6 |
foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); |
7 |
else
|
8 |
foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); |
9 |
}
|
Jika kita tidak menemukan rintangan, kita bisa menggeser objek.
1 |
while (!foundObstacleX && offset != 0.0f) |
2 |
{
|
3 |
offset -= step; |
4 |
|
5 |
if (step > 0.0f) |
6 |
foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); |
7 |
else
|
8 |
foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); |
9 |
|
10 |
if (!foundObstacleX) |
11 |
{
|
12 |
position.x += step; |
13 |
topRight.x += step; |
14 |
bottomLeft.x += step; |
15 |
}
|
16 |
}
|
Akhirnya, setelah kita bergerak, kita periksa tabrakan atas dan bawah, karena kita bisa saja melewati bagian bawah atau atas sebuah blok. Ini hanya untuk memperbarui status poisisi agar tetap akurat.
1 |
public void MoveX(ref Vector2 position, ref bool foundObstacleX, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) |
2 |
{
|
3 |
while (!foundObstacleX && offset != 0.0f) |
4 |
{
|
5 |
offset -= step; |
6 |
|
7 |
if (step > 0.0f) |
8 |
foundObstacleX = CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state, true); |
9 |
else
|
10 |
foundObstacleX = CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state, true); |
11 |
|
12 |
if (!foundObstacleX) |
13 |
{
|
14 |
position.x += step; |
15 |
topRight.x += step; |
16 |
bottomLeft.x += step; |
17 |
|
18 |
CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); |
19 |
CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); |
20 |
}
|
21 |
}
|
22 |
}
|
Fungsi MoveY
bekerja dengan cara yang kurang lebih sama.
1 |
public void MoveY(ref Vector2 position, ref bool foundObstacleY, float offset, float step, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) |
2 |
{
|
3 |
while (!foundObstacleY && offset != 0.0f) |
4 |
{
|
5 |
offset -= step; |
6 |
|
7 |
if (step > 0.0f) |
8 |
foundObstacleY = CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); |
9 |
else
|
10 |
foundObstacleY = CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); |
11 |
|
12 |
if (!foundObstacleY) |
13 |
{
|
14 |
position.y += step; |
15 |
topRight.y += step; |
16 |
bottomLeft.y += step; |
17 |
|
18 |
CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state); |
19 |
CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state); |
20 |
}
|
21 |
}
|
22 |
}
|
Menggabungkan gerakan
Sekarang kita memiliki fungsi yang bertanggung jawab pada gerakan vertikal dan horizontal, kita bisa membuat fungsi utama untuk pergerakan objek.
1 |
public void Move(Vector2 offset, Vector2 speed, ref Vector2 position, ref Vector2 remainder, AABB aabb, ref PositionState state) |
2 |
{
|
3 |
}
|
Fungsi akan menerima nilai seberapa jauh objek bergerak, kecepatan objek saat ini, posisi dan sisa dari posisi saat ini, AABB objek, dan status posisi.
Pertama kita perlu menambahkan offset ke nilai sisa, jadi kita memiliki nilai keseluruhan seberapa banyak karakter perlu bergerak.
1 |
remainder += offset; |
Karena kita akan memanggil fungsi MoveX
dan MoveY
di dalam fungsi ini, kita perlu mengirim nilai pojok kanan atas dan kiri bawah AABB, kita perhitungkan sekarang.
1 |
Vector2 topRight = aabb.Max(); |
2 |
Vector2 bottomLeft = aabb.Min(); |
Kita juga perlu mengambil vektor step. Ini akan digunakan sebagai arah gerakan objek.
1 |
var step = new Vector2(Mathf.Sign(offset.x), Mathf.Sign(offset.y)); |
Sekarang kita lihat berapa banyak piksel kita perlu bergerak. kita hanya perlu membulatkan nilai sisa, karena kita akan selalu bergerak dalam bilangan bulat, lalu kita perlu menguranginya dari nilai sisa tersebut.
1 |
var move = new Vector2(Mathf.Round(remainder.x), Mathf.Round(remainder.y)); |
2 |
remainder -= move; |
Sekarang kita bagi gerakan menjadi empat kasus, tergantung dari nilai vektor gerakan kita. Jika nilai x dan y pada vektor gerakan adalah 0, tidak ada gerakan yang perlu dilakukan, jadi kita cukup keluar dari fungsi.
1 |
if (move.x == 0.0f && move.y == 0.0f) |
2 |
return; |
Jika hanya nilai Y yang 0, kita hanya akan bergerak secara horizontal.
1 |
if (move.x == 0.0f && move.y == 0.0f) |
2 |
return; |
3 |
else if (move.x != 0.0f && move.y == 0.0f) |
4 |
MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state); |
Jika hanya nilai x yang bernilai 0, kita hanya akan bergerak secara vertikal.
1 |
if (move.x == 0.0f && move.y == 0.0f) |
2 |
return; |
3 |
else if (move.x != 0.0f && move.y == 0.0f) |
4 |
MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state); |
5 |
else if (move.y != 0.0f && move.x == 0.0f) |
6 |
MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state); |
7 |
else
|
8 |
{
|
9 |
}
|
Jika kita perlu bergerak pada sumbu X dan Y, kita perlu bergerak sesuai dengan pola yang dijelaskan sebelumnya. Pertama kita hitung rasio kecepatan objek.
1 |
float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); |
Kita buat juga akumulasi vertikal yang akan menyimpan berapa banyak piksel kita perlu bergerak vertikal dalam setiap pengulangan.
1 |
float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); |
2 |
float vertAccum = 0.0f; |
Kondisi untuk berhenti bergerak adalah jika kita bertemu rintangan di sumbu apa saja, atau objek sudah digerakkan sesuai dengan vektor gerak secara penuh.
1 |
float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); |
2 |
float vertAccum = 0.0f; |
3 |
|
4 |
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) |
5 |
{
|
6 |
}
|
Sekarang kita hitung seberapa banyak piksel kita perlu gerakkan objek secara vertikal.
1 |
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) |
2 |
{
|
3 |
vertAccum += Mathf.Sign(step.y) * speedRatio; |
4 |
}
|
Untuk gerakannya, kita perlu begerak satu step secara horizontal.
1 |
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) |
2 |
{
|
3 |
vertAccum += Mathf.Sign(step.y) * speedRatio; |
4 |
|
5 |
MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); |
6 |
move.x -= step.x; |
7 |
}
|
8 |
Setelah ini kita bisa bergerak vertikal. Di sini kita tahu bahwa kita perlu menggeser objek sesuai nilai dalam vertAccum
, tapi jika tidak akurat, jika kita bergerak sepenuhnya pada sumbu X, kita perlu bergerak sepenuhnya juga pada sumbu Y.
1 |
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) |
2 |
{
|
3 |
vertAccum += Mathf.Sign(step.y) * speedRatio; |
4 |
|
5 |
MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); |
6 |
move.x -= step.x; |
7 |
|
8 |
while (!foundObstacleY && move.y != 0.0f && (Mathf.Abs(vertAccum) >= 1.0f || move.x == 0.0f)) |
9 |
{
|
10 |
move.y -= step.y; |
11 |
vertAccum -= step.y; |
12 |
|
13 |
MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state); |
14 |
}
|
15 |
}
|
16 |
Lengkapnya, fungsi tersebut akan terlihat seperti ini:
1 |
public void Move(Vector2 offset, Vector2 speed, ref Vector2 position, ref Vector2 remainder, AABB aabb, ref PositionState state) |
2 |
{
|
3 |
remainder += offset; |
4 |
|
5 |
Vector2 topRight = aabb.Max(); |
6 |
Vector2 bottomLeft = aabb.Min(); |
7 |
|
8 |
bool foundObstacleX = false, foundObstacleY = false; |
9 |
|
10 |
var step = new Vector2(Mathf.Sign(offset.x), Mathf.Sign(offset.y)); |
11 |
var move = new Vector2(Mathf.Round(remainder.x), Mathf.Round(remainder.y)); |
12 |
remainder -= move; |
13 |
|
14 |
if (move.x == 0.0f && move.y == 0.0f) |
15 |
return; |
16 |
else if (move.x != 0.0f && move.y == 0.0f) |
17 |
MoveX(ref position, ref foundObstacleX, move.x, step.x, ref topRight, ref bottomLeft, ref state); |
18 |
else if (move.y != 0.0f && move.x == 0.0f) |
19 |
MoveY(ref position, ref foundObstacleY, move.y, step.y, ref topRight, ref bottomLeft, ref state); |
20 |
else
|
21 |
{
|
22 |
float speedRatio = Mathf.Abs(speed.y) / Mathf.Abs(speed.x); |
23 |
float vertAccum = 0.0f; |
24 |
|
25 |
while (!foundObstacleX && !foundObstacleY && (move.x != 0.0f || move.y != 0.0f)) |
26 |
{
|
27 |
vertAccum += Mathf.Sign(step.y) * speedRatio; |
28 |
|
29 |
MoveX(ref position, ref foundObstacleX, step.x, step.x, ref topRight, ref bottomLeft, ref state); |
30 |
move.x -= step.x; |
31 |
|
32 |
while (!foundObstacleY && move.y != 0.0f && (Mathf.Abs(vertAccum) >= 1.0f || move.x == 0.0f)) |
33 |
{
|
34 |
move.y -= step.y; |
35 |
vertAccum -= step.y; |
36 |
|
37 |
MoveY(ref position, ref foundObstacleX, step.y, step.y, ref topRight, ref bottomLeft, ref state); |
38 |
}
|
39 |
}
|
40 |
}
|
41 |
}
|
Sekarang kita bisa menggunakan fungsi yang kita buat untuk menyusun fungsi UpdatePhysics
.
Membuat fungsi memperbarui Physics
Pertama, kita perlu memperbarui status posisi, jadi semua data frame sebelumnya disimpan ke variabel yang sesuai, dan data frame ini diatur ulang.
1 |
public void UpdatePhysics() |
2 |
{
|
3 |
mPS.pushedBottom = mPS.pushesBottom; |
4 |
mPS.pushedRight = mPS.pushesRight; |
5 |
mPS.pushedLeft = mPS.pushesLeft; |
6 |
mPS.pushedTop = mPS.pushesTop; |
7 |
|
8 |
mPS.pushedBottomTile = mPS.pushesBottomTile; |
9 |
mPS.pushedLeftTile = mPS.pushesLeftTile; |
10 |
mPS.pushedRightTile = mPS.pushesRightTile; |
11 |
mPS.pushedTopTile = mPS.pushesTopTile; |
12 |
|
13 |
mPS.pushesBottom = mPS.pushesLeft = mPS.pushesRight = mPS.pushesTop = |
14 |
mPS.pushesBottomTile = mPS.pushesLeftTile = mPS.pushesRightTile = mPS.pushesTopTile = |
15 |
mPS.pushesBottomObject = mPS.pushesLeftObject = mPS.pushesRightObject = mPS.pushesTopObject = mPS.onOneWay = false; |
16 |
}
|
Sekarang kita perbarui status tabrakan objek kita. Kita lakukan sebelum menggeser objek agar kita punya data terbaru apakah objek ada di atas tanah atau sedang mendorong petak lain. Biasanya data frame sebelumnya akan tetap terbarui jika tanah tidak bisa dimodifikasi dan objek lain tidak bisa bergerak, tapi kita asumsikan hal-hal tersebut bisa terjadi.
1 |
Vector2 topRight = mAABB.Max(); |
2 |
Vector2 bottomLeft = mAABB.Min(); |
3 |
|
4 |
CollidesWithTiles(ref mPosition, ref topRight, ref bottomLeft, ref mPS); |
CollidesWithTiles
hanya memanggil semua fungsi tabrakan yang sudah kita tulis.
1 |
public void CollidesWithTiles(ref Vector2 position, ref Vector2 topRight, ref Vector2 bottomLeft, ref PositionState state) |
2 |
{
|
3 |
CollidesWithTileTop(ref position, ref topRight, ref bottomLeft, ref state); |
4 |
CollidesWithTileBottom(ref position, ref topRight, ref bottomLeft, ref state); |
5 |
CollidesWithTileLeft(ref position, ref topRight, ref bottomLeft, ref state); |
6 |
CollidesWithTileRight(ref position, ref topRight, ref bottomLeft, ref state); |
7 |
}
|
Lalu perbarui kecepatannya.
1 |
mOldSpeed = mSpeed; |
2 |
|
3 |
if (mPS.pushesBottomTile) |
4 |
mSpeed.y = Mathf.Max(0.0f, mSpeed.y); |
5 |
if (mPS.pushesTopTile) |
6 |
mSpeed.y = Mathf.Min(0.0f, mSpeed.y); |
7 |
if (mPS.pushesLeftTile) |
8 |
mSpeed.x = Mathf.Max(0.0f, mSpeed.x); |
9 |
if (mPS.pushesRightTile) |
10 |
mSpeed.x = Mathf.Min(0.0f, mSpeed.x); |
Dan perbarui posisi. Pertama, kita simpan posisi lama.
1 |
mOldPosition = mPosition; |
Hitung posisi baru.
1 |
Vector2 newPosition = mPosition + mSpeed * Time.deltaTime; |
Hitung offset antara dua posisi tersebut.
1 |
Vector2 offset = newPosition - mPosition; |
Jika offset tersebut bukan nol, kita bisa panggil fungsi Move.
1 |
if (offset != Vector2.zero) |
2 |
Move(offset, mSpeed, ref mPosition, ref mRemainder, mAABB, ref mPS); |
Akhirnya, perbarui AABB objek dan status posisi.
1 |
mAABB.Center = mPosition; |
2 |
|
3 |
mPS.pushesBottom = mPS.pushesBottomTile; |
4 |
mPS.pushesRight = mPS.pushesRightTile; |
5 |
mPS.pushesLeft = mPS.pushesLeftTile; |
6 |
mPS.pushesTop = mPS.pushesTopTile; |
Sekian! Sistem ini sekarang menggantikan yang lama, hasilnya akan sama walau cara kita melakukannya berbeda.
Ringkasan
Dengan begitu kita sudah menyiapkan dasar untuk bidang miring, yang tersisa adalah mengisi celah pada pengecekan tabrakan. Kita sudah menyelesaikan penyimpanan data di sini dan menghilangkan banyak kompleksitas geometri dengan mengimplementasi integrasi gerakan satu piksel.
Ini akan membuat implementasi bidang miring lebih mudah dibandingkan yang kita perlu lakukan sebelumnya. Kita akan menyelesaikan pekerjaan itu pada bagian berikutnya dari seri tutorial ini.
Terima kasih sudah membaca!