Praktik input untuk game
Topik ini menjelaskan pola dan teknik untuk menggunakan perangkat input secara efektif dalam game Platform Windows Universal (UWP).
Dengan membaca topik ini, Anda akan mempelajari:
- cara melacak pemutar dan perangkat input dan navigasi mana yang saat ini mereka gunakan
- cara mendeteksi transisi tombol (ditekan ke dirilis, dilepaskan ke ditekan)
- cara mendeteksi pengaturan tombol kompleks dengan satu pengujian
Memilih kelas perangkat input
Ada banyak jenis API input yang tersedia untuk Anda, seperti ArcadeStick, FlightStick, dan Gamepad. Bagaimana Anda memutuskan API mana yang akan digunakan untuk game Anda?
Anda harus memilih API mana pun yang memberi Anda input yang paling tepat untuk game Anda. Misalnya, jika Anda membuat game platform 2D, Anda mungkin hanya dapat menggunakan kelas Gamepad dan tidak repot dengan fungsionalitas tambahan yang tersedia melalui kelas lain. Ini akan membatasi permainan hanya untuk mendukung gamepad dan menyediakan antarmuka yang konsisten yang akan bekerja di banyak gamepad yang berbeda tanpa perlu kode tambahan.
Di sisi lain, untuk simulasi penerbangan dan balap yang kompleks, Anda mungkin ingin menghitung semua objek RawGameController sebagai garis besar untuk memastikan mereka mendukung perangkat niche apa pun yang mungkin dimiliki pemain peminat, termasuk perangkat seperti pedal terpisah atau pembatasan yang masih digunakan oleh satu pemutar.
Dari sana, Anda dapat menggunakan metode FromGameController kelas input, seperti Gamepad.FromGameController, untuk melihat apakah setiap perangkat memiliki tampilan yang lebih dikumpulkan. Misalnya, jika perangkat juga merupakan Gamepad, maka Anda mungkin ingin menyesuaikan UI pemetaan tombol untuk mencerminkan itu, dan menyediakan beberapa pemetaan tombol default yang masuk akal untuk dipilih. (Ini berbeda dengan mengharuskan pemutar mengonfigurasi input gamepad secara manual jika Anda hanya menggunakan RawGameController.)
Atau, Anda dapat melihat ID vendor (VID) dan ID produk (PID) dari RawGameController (menggunakan HardwareVendorId dan HardwareProductId, masing-masing) dan menyediakan pemetaan tombol yang disarankan untuk perangkat populer sambil tetap kompatibel dengan perangkat yang tidak diketahui yang keluar di masa depan melalui pemetaan manual oleh pemutar.
Melacak pengontrol yang terhubung
Meskipun setiap jenis pengontrol menyertakan daftar pengontrol yang terhubung (seperti Gamepad.Gamepads), sebaiknya pertahankan daftar pengontrol Anda sendiri. Lihat Daftar gamepad untuk informasi lebih lanjut (setiap jenis pengontrol memiliki bagian bernama serupa pada topiknya sendiri).
Namun, apa yang terjadi ketika pemutar mencolokkan pengontrol mereka, atau mencolokkan yang baru? Anda perlu menangani peristiwa ini, dan memperbarui daftar Anda dengan sesuai. Lihat Menambahkan dan menghapus gamepad untuk informasi selengkapnya (sekali lagi, setiap jenis pengontrol memiliki bagian bernama serupa pada topiknya sendiri).
Karena peristiwa yang ditambahkan dan dihapus dimunculkan secara asinkron, Anda bisa mendapatkan hasil yang salah saat berhadapan dengan daftar pengontrol Anda. Oleh karena itu, kapan saja Anda mengakses daftar pengontrol, Anda harus menguncinya sehingga hanya satu utas yang dapat mengaksesnya pada satu waktu. Ini dapat dilakukan dengan Runtime Konkurensi, khususnya kelas critical_section, di <ppl.h>.
Hal lain yang perlu dipikirkan adalah bahwa daftar pengontrol yang terhubung awalnya akan kosong, dan membutuhkan waktu satu atau dua detik untuk diisi. Jadi jika Anda hanya menetapkan gamepad saat ini dalam metode mulai, itu akan null!
Untuk memperbaiki hal ini, Anda harus memiliki metode yang "menyegarkan" gamepad utama (dalam permainan pemain tunggal; game multipemain akan membutuhkan solusi yang lebih canggih). Anda kemudian harus memanggil metode ini di pengontrol Anda yang ditambahkan dan pengontrol menghapus penanganan aktivitas, atau dalam metode pembaruan Anda.
Metode berikut hanya mengembalikan gamepad pertama dalam daftar (atau nullptr jika daftar kosong). Kemudian Anda hanya perlu mengingat untuk memeriksa nullptr kapan saja Anda melakukan apa pun dengan pengontrol. Terserah Anda apakah Anda ingin memblokir gameplay ketika tidak ada pengontrol yang terhubung (misalnya, dengan menjeda permainan) atau hanya memiliki gameplay berlanjut, sambil mengabaikan input.
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Gaming::Input;
using namespace concurrency;
Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
Menggabungkan semuanya, berikut adalah contoh cara menangani input dari gamepad:
#include <algorithm>
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Foundation;
using namespace Windows::Gaming::Input;
using namespace concurrency;
static Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
static Gamepad^ m_gamepad = nullptr;
static critical_section m_lock{};
void Start()
{
// Register for gamepad added and removed events.
Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(&OnGamepadAdded);
Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(&OnGamepadRemoved);
// Add connected gamepads to m_myGamepads.
for (auto gamepad : Gamepad::Gamepads)
{
OnGamepadAdded(nullptr, gamepad);
}
}
void Update()
{
// Update the current gamepad if necessary.
if (m_gamepad == nullptr)
{
auto gamepad = GetFirstGamepad();
if (m_gamepad != gamepad)
{
m_gamepad = gamepad;
}
}
if (m_gamepad != nullptr)
{
// Gather gamepad reading.
}
}
// Get the first gamepad in the list.
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
void OnGamepadAdded(Platform::Object^ sender, Gamepad^ args)
{
// Check if the just-added gamepad is already in m_myGamepads; if it isn't,
// add it.
critical_section::scoped_lock lock{ m_lock };
auto it = std::find(begin(m_myGamepads), end(m_myGamepads), args);
if (it == end(m_myGamepads))
{
m_myGamepads->Append(args);
}
}
void OnGamepadRemoved(Platform::Object^ sender, Gamepad^ args)
{
// Remove the gamepad that was just disconnected from m_myGamepads.
unsigned int indexRemoved;
critical_section::scoped_lock lock{ m_lock };
if (m_myGamepads->IndexOf(args, &indexRemoved))
{
if (m_gamepad == m_myGamepads->GetAt(indexRemoved))
{
m_gamepad = nullptr;
}
m_myGamepads->RemoveAt(indexRemoved);
}
}
Melacak pengguna dan perangkat mereka
Semua perangkat input dikaitkan dengan Pengguna sehingga identitas mereka dapat ditautkan ke gameplay, pencapaian, perubahan pengaturan, dan aktivitas lainnya. Pengguna dapat masuk atau keluar sesering mungkin, dan umum bagi pengguna lain untuk masuk pada perangkat input yang tetap terhubung ke sistem setelah pengguna sebelumnya keluar. Saat pengguna masuk atau keluar, peristiwa IGameController.UserChanged dinaikkan . Anda dapat mendaftarkan penanganan aktivitas untuk kejadian ini untuk melacak pemutar dan perangkat yang mereka gunakan.
Identitas pengguna juga merupakan cara perangkat input dikaitkan dengan pengontrol navigasi UI yang sesuai.
Untuk alasan ini, input pemutar harus dilacak dan berkorelasi dengan properti Pengguna dari kelas perangkat (diwarisi dari antarmuka IGameController ).
Aplikasi sampel UserGamepadPairingUWP di GitHub menunjukkan bagaimana Anda dapat melacak pengguna dan perangkat yang mereka gunakan.
Mendeteksi transisi tombol
Terkadang Anda ingin tahu kapan tombol pertama kali ditekan atau dirilis; yaitu, tepatnya ketika status tombol beralih dari dilepaskan ke ditekan atau dari ditekan ke dilepaskan. Untuk menentukan ini, Anda perlu mengingat pembacaan perangkat sebelumnya dan membandingkan pembacaan saat ini terhadapnya untuk melihat apa yang berubah.
Contoh berikut menunjukkan pendekatan dasar untuk mengingat bacaan sebelumnya; gamepad ditampilkan di sini, tetapi prinsipnya sama untuk tongkat arkade, roda balap, dan jenis perangkat input lainnya.
Gamepad gamepad;
GamepadReading newReading();
GamepadReading oldReading();
// Called at the start of the game.
void Game::Start()
{
gamepad = Gamepad::Gamepads[0];
}
// Game::Loop represents one iteration of a typical game loop
void Game::Loop()
{
// move previous newReading into oldReading before getting next newReading
oldReading = newReading, newReading = gamepad.GetCurrentReading();
// process device readings using buttonJustPressed/buttonJustReleased (see below)
}
Sebelum melakukan hal lain, Game::Loop
memindahkan nilai yang ada (pembacaan newReading
gamepad dari iterasi perulangan sebelumnya) ke , oldReading
lalu mengisi newReading
dengan pembacaan gamepad baru untuk iterasi saat ini. Ini memberi Anda informasi yang Anda butuhkan untuk mendeteksi transisi tombol.
Contoh berikut menunjukkan pendekatan dasar untuk mendeteksi transisi tombol:
bool ButtonJustPressed(const GamepadButtons selection)
{
bool newSelectionPressed = (selection == (newReading.Buttons & selection));
bool oldSelectionPressed = (selection == (oldReading.Buttons & selection));
return newSelectionPressed && !oldSelectionPressed;
}
bool ButtonJustReleased(GamepadButtons selection)
{
bool newSelectionReleased =
(GamepadButtons.None == (newReading.Buttons & selection));
bool oldSelectionReleased =
(GamepadButtons.None == (oldReading.Buttons & selection));
return newSelectionReleased && !oldSelectionReleased;
}
Kedua fungsi ini pertama kali memperoleh status Boolean dari pemilihan tombol dari newReading
dan oldReading
, lalu lakukan logika Boolean untuk menentukan apakah transisi target telah terjadi. Fungsi-fungsi ini mengembalikan true hanya jika pembacaan baru berisi status target (masing-masing ditekan atau dirilis) dan pembacaan lama tidak juga berisi status target; jika tidak, mereka mengembalikan false.
Mendeteksi pengaturan tombol kompleks
Setiap tombol perangkat input menyediakan pembacaan digital yang menunjukkan apakah perangkat ditekan (ke bawah) atau dilepaskan (ke atas). Untuk efisiensi, pembacaan tombol tidak direpresentasikan sebagai nilai boolean individual; sebaliknya, semuanya dikemas ke dalam bitfield yang diwakili oleh enumerasi khusus perangkat seperti GamepadButtons. Untuk membaca tombol tertentu, masking bitwise digunakan untuk mengisolasi nilai yang Anda minati. Tombol ditekan (ke bawah) ketika bit yang sesuai diatur; jika tidak, itu dirilis (naik).
Ingat bagaimana satu tombol ditentukan untuk ditekan atau dilepaskan; gamepad ditampilkan di sini, tetapi prinsipnya sama untuk tongkat arkade, roda balap, dan jenis perangkat input lainnya.
GamepadReading reading = gamepad.GetCurrentReading();
// Determines whether gamepad button A is pressed.
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
// The A button is pressed.
}
// Determines whether gamepad button A is released.
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
// The A button is released (not pressed).
}
Seperti yang Anda lihat, menentukan status satu tombol lurus ke depan, tetapi terkadang Anda mungkin ingin menentukan apakah beberapa tombol ditekan atau dilepaskan, atau jika satu set tombol diatur dengan cara tertentu—beberapa ditekan, beberapa tidak. Menguji beberapa tombol lebih kompleks daripada menguji tombol tunggal—terutama dengan potensi status tombol campuran—tetapi ada rumus sederhana untuk pengujian ini yang berlaku untuk pengujian tombol tunggal dan beberapa.
Contoh berikut menentukan apakah tombol gamepad A dan B keduanya ditekan:
if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both pressed.
}
Contoh berikut menentukan apakah tombol gamepad A dan B keduanya dirilis:
if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both released (not pressed).
}
Contoh berikut menentukan apakah tombol gamepad A ditekan saat tombol B dirilis:
if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A button is pressed and the B button is released (B is not pressed).
}
Rumus yang kelima contoh ini memiliki kesamaan adalah bahwa susunan tombol yang akan diuji ditentukan oleh ekspresi di sisi kiri operator kesetaraan sementara tombol yang akan dipertimbangkan dipilih oleh ekspresi masking di sisi kanan.
Contoh berikut menunjukkan rumus ini dengan lebih jelas dengan menulis ulang contoh sebelumnya:
auto buttonArrangement = GamepadButtons::A;
auto buttonSelection = (reading.Buttons & (GamepadButtons::A | GamepadButtons::B));
if (buttonArrangement == buttonSelection)
{
// The A button is pressed and the B button is released (B is not pressed).
}
Rumus ini dapat diterapkan untuk menguji sejumlah tombol dalam pengaturan status apa pun.
Mendapatkan status baterai
Untuk pengontrol game apa pun yang mengimplementasikan antarmuka IGameControllerBatteryInfo , Anda dapat memanggil TryGetBatteryReport pada instans pengontrol untuk mendapatkan objek BatteryReport yang menyediakan informasi tentang baterai di pengontrol. Anda bisa mendapatkan properti seperti laju pengisian daya baterai (ChargeRateInMilliwatts), perkiraan kapasitas energi baterai baru (DesignCapacityInMilliwattHours), dan kapasitas energi yang terisi penuh dari baterai saat ini (FullChargeCapacityInMilliwattHours).
Untuk pengontrol game yang mendukung pelaporan baterai terperinci, Anda bisa mendapatkan ini dan informasi lebih lanjut tentang baterai, sebagaimana dirinci dalam Mendapatkan informasi baterai. Namun, sebagian besar pengontrol game tidak mendukung tingkat pelaporan baterai tersebut, dan sebaliknya menggunakan perangkat keras berkode rendah. Untuk pengontrol ini, Anda harus mengingat pertimbangan berikut:
ChargeRateInMilliwatts dan DesignCapacityInMilliwattHours akan selalu NULL.
Anda bisa mendapatkan persentase baterai dengan menghitung RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours. Anda harus mengabaikan nilai properti ini dan hanya menangani persentase terhitung.
Persentase dari poin sebelumnya akan selalu menjadi salah satu hal berikut:
- 100% (Penuh)
- 70% (Sedang)
- 40% (Rendah)
- 10% (Kritis)
Jika kode Anda melakukan beberapa tindakan (seperti menggambar UI) berdasarkan persentase sisa masa pakai baterai, pastikan kode tersebut sesuai dengan nilai di atas. Misalnya, jika Anda ingin memperingatkan pemutar ketika baterai pengontrol rendah, lakukan ketika mencapai 10%.