Nilai kecepatan non-integer - apakah ada cara yang lebih bersih untuk melakukan ini?

21

Seringkali saya ingin menggunakan nilai kecepatan seperti 2,5 untuk memindahkan karakter saya dalam game berbasis pixel. Deteksi tabrakan umumnya akan lebih sulit jika saya melakukannya. Jadi saya akhirnya melakukan sesuatu seperti ini:

moveX(2);
if (ticks % 2 == 0) { // or if (moveTime % 2 == 0)
    moveX(1);
}

Saya merasa ngeri di dalam setiap kali saya harus menulis itu, apakah ada cara yang lebih bersih untuk memindahkan karakter dengan nilai kecepatan non-integer atau akankah saya tetap terjebak melakukan ini selamanya?

Aki
sumber
11
Sudahkah Anda mempertimbangkan untuk membuat unit integer Anda lebih kecil (mis. 1/10 dari unit display) kemudian 2,5 diterjemahkan menjadi 25, dan Anda masih bisa memperlakukannya sebagai integer untuk semua pemeriksaan dan memperlakukan setiap frame secara konsisten.
DMGregory
6
Anda dapat mempertimbangkan mengadopsi algoritma garis Bresenham yang dapat diimplementasikan hanya dengan menggunakan bilangan bulat.
n0rd
1
Ini biasa dilakukan pada konsol 8bit lama. Lihat artikel seperti ini untuk contoh tentang bagaimana gerakan subpixel diimplementasikan dengan metode titik tetap: tasvideos.org/GameResources/NES/BattleOfOlympus.html
Lucas

Jawaban:

13

Bresenham

Di masa lalu, ketika orang masih menulis rutin video dasar mereka sendiri untuk menggambar garis dan lingkaran, itu tidak pernah terdengar menggunakan algoritma garis Bresenham untuk itu.

Bresenham memecahkan masalah ini: Anda ingin menggambar garis pada layar yang menggerakkan dxpiksel dalam arah horizontal sementara pada saat yang sama menjangkau dypiksel dalam arah vertikal. Ada karakter "ringan" yang melekat pada garis; bahkan jika Anda memiliki piksel bilangan bulat, Anda berakhir dengan kecenderungan rasional.

Algoritme harus cepat, yang artinya dapat menggunakan integer aritmatika saja; dan itu juga hilang tanpa penggandaan atau pembagian, hanya penambahan dan pengurangan.

Anda dapat menyesuaikan itu untuk kasus Anda:

  • "Arah x" Anda (dalam hal algoritma Bresenham) adalah jam Anda.
  • "Arah y" Anda adalah nilai yang ingin Anda tambahkan (yaitu, posisi karakter Anda - hati-hati, ini sebenarnya bukan "y" dari sprite Anda atau apa pun di layar, lebih merupakan nilai abstrak)

"x / y" di sini bukan lokasi di layar, tetapi nilai salah satu dimensi Anda dalam waktu. Jelas, jika sprite Anda berjalan dengan arah yang sewenang-wenang melintasi layar, Anda akan memiliki beberapa Bresenhams yang berjalan secara terpisah, 2 untuk 2D, 3 untuk 3D.

Contoh

Katakanlah Anda ingin memindahkan karakter Anda dalam gerakan sederhana dari 0 hingga 25 di sepanjang salah satu sumbu Anda. Saat bergerak dengan kecepatan 2.5, ia akan muncul di frame 10.

Ini sama dengan "menggambar garis" dari (0,0) hingga (10,25). Ambil algoritme garis Bresenham dan biarkan berjalan. Jika Anda melakukannya dengan benar (dan ketika Anda mempelajarinya, itu akan dengan cepat menjadi jelas bagaimana Anda melakukannya dengan benar), maka itu akan menghasilkan 11 "poin" untuk Anda (0,0), (1,2), (2, 5), (3,7), (4,10) ... (10,25).

Petunjuk adaptasi

Jika Anda google algoritma itu dan menemukan beberapa kode (Wikipedia memiliki perjanjian yang cukup besar di dalamnya), ada beberapa hal yang perlu Anda perhatikan:

  • Ini jelas bekerja untuk semua jenis dxdan dy. Anda tertarik pada satu kasus khusus (yaitu, Anda tidak akan pernah memilikinya dx=0).
  • Implementasi yang biasa akan memiliki beberapa kasus yang berbeda untuk kuadran di layar, tergantung pada apakah dxdan dypositif, negatif, dan juga apakah abs(dx)>abs(dy)atau tidak. Anda tentu juga memilih apa yang Anda butuhkan di sini. Anda harus memastikan secara khusus bahwa arah yang ditingkatkan oleh 1setiap tick selalu merupakan arah "jam" Anda.

Jika Anda menerapkan penyederhanaan ini, hasilnya akan sangat sederhana, dan singkirkan semua realita.

AnoE
sumber
1
Ini harus menjadi jawaban yang diterima. Setelah memprogram game pada C64 di 80-an, dan fraktal pada PC di 90-an, saya masih merasa ngeri menggunakan floating point di mana pun saya bisa menghindarinya. Atau tentu saja, dengan FPU di mana-mana di prosesor hari ini, argumen kinerja sebagian besar diperdebatkan, tetapi aritmatika floating point masih membutuhkan lebih banyak transistor, menarik lebih banyak daya, dan banyak prosesor akan mematikan FPU mereka sepenuhnya saat tidak digunakan. Jadi menghindari floating point akan membuat pengguna ponsel Anda berterima kasih karena tidak menyedot baterai mereka begitu cepat.
Guntram Blohm mendukung Monica
@ GuntramBlohm Jawaban yang diterima berfungsi dengan baik saat menggunakan Fixed Point juga, yang menurut saya adalah cara yang baik untuk melakukannya. Bagaimana perasaan Anda tentang nomor titik tetap?
leetNightshade
Mengubah ini menjadi jawaban yang diterima setelah mengetahui ini adalah bagaimana mereka melakukannya dalam hari 8-bit dan 16-bit.
Akumulator
26

Ada cara hebat untuk melakukan apa yang Anda inginkan.

Selain floatkecepatan, Anda harus memiliki floatvariabel kedua yang akan berisi dan mengakumulasikan perbedaan antara kecepatan nyata dan kecepatan bulat . Perbedaan ini kemudian dikombinasikan dengan kecepatan itu sendiri.

#include <iostream>
#include <cmath>

int main()
{
    int pos = 10; 
    float vel = 0.3, vel_lag = 0;

    for (int i = 0; i < 20; i++)   
    {
        float real_vel = vel + vel_lag;
        int int_vel = std::lround(real_vel);
        vel_lag = real_vel - int_vel;

        std::cout << pos << ' ';
        pos += int_vel;
    }
}

Keluaran:

10 10 11 11 11 12 12 12 12 13 13 13 14 14 14 15 15 15 15 16 16

HolyBlackCat
sumber
5
Mengapa Anda lebih suka solusi ini daripada menggunakan float (atau fixedpoint) untuk kecepatan dan posisi dan hanya membulatkan posisi ke seluruh bilangan bulat di akhir?
CodesInChaos
@CodesInChaos Saya tidak suka solusi saya daripada yang itu. Ketika saya menulis jawaban ini, saya tidak tahu.
HolyBlackCat
16

Gunakan nilai mengambang untuk gerakan dan nilai integer untuk tabrakan dan rendering.

Ini sebuah contoh:

class Character {
    float position;
public:
    void move(float delta) {
        this->position += delta;
    }
    int getPosition() const {
        return lround(this->position);
    }
};

Ketika Anda bergerak, Anda menggunakan move()yang mengakumulasi posisi fraksional. Tetapi tabrakan dan rendering dapat menangani posisi integral dengan menggunakan getPosition()fungsi tersebut.

congusbongus
sumber
Perhatikan bahwa dalam kasus game berjaringan, menggunakan tipe floating point untuk simulasi dunia mungkin sulit. Lihat misalnya gafferongames.com/networking-for-game-programmers/… .
liori
@liori Jika Anda memiliki kelas titik tetap yang bertindak seperti pengganti drop-in untuk float, bukankah itu sebagian besar menyelesaikan masalah tersebut?
leetNightshade
@leetNightshade: tergantung pada implementasinya.
liori
1
Saya akan mengatakan masalah ini tidak ada dalam praktiknya, perangkat keras apa yang mampu menjalankan game jaringan modern yang tidak memiliki IEEE 754 floats ???
Sopel