Bagikan melalui


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 penggemar, termasuk perangkat seperti pedal atau pembatasan terpisah 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 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 pemain untuk mengonfigurasi input gamepad secara manual jika Anda hanya menggunakan RawGameController.)

Atau, Anda dapat melihat ID vendor (VID) dan ID produk (PID) dari RawGameController (masing-masing menggunakan HardwareVendorId dan HardwareProductId) dan menyediakan pemetaan tombol yang disarankan untuk perangkat populer sambil tetap kompatibel dengan perangkat tidak dikenal 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), ada baiknya untuk mempertahankan daftar pengontrol Anda sendiri. Lihat Daftar gamepad untuk informasi selengkapnya (setiap jenis pengontrol memiliki bagian bernama serupa pada topiknya sendiri).

Namun, apa yang terjadi ketika pemutar mencolokkan pengontrol mereka, atau mencolokkan pengontrol baru? Anda perlu menangani peristiwa ini, dan memperbarui daftar Anda dengan sesuai. Lihat Menambahkan dan menghapus gamepad untuk informasi lebih lanjut (sekali lagi, setiap jenis pengontrol memiliki bagian bernama serupa pada topiknya sendiri).

Karena peristiwa yang ditambahkan dan dihapus dinaikkan 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 Concurrency Runtime, 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 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 ditambahkan dan pengontrol dihapus penanganan aktivitas, atau dalam metode pembaruan Anda.

Metode berikut hanya mengembalikan gamepad pertama dalam daftar (atau nullptr jika daftar kosong). Maka Anda hanya perlu ingat 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;
}

Menyatukan 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 acara 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 dilepaskan; yaitu, tepatnya ketika status tombol beralih dari dilepaskan ke ditekan atau dari ditekan untuk dilepaskan. Untuk menentukan hal 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 pembacaan sebelumnya; gamepad ditampilkan di sini, tetapi prinsip-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 melakukan 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 itu ditekan (turun) atau dilepaskan (naik). 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 tombol tunggal ditentukan untuk ditekan atau dilepaskan; gamepad ditampilkan di sini, tetapi prinsip-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 satu dan beberapa pengujian tombol.

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 memiliki kesamaan kelima contoh ini 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 statusnya.

Mendapatkan status baterai

Untuk setiap pengontrol game 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 baterai yang terisi penuh saat ini (FullChargeCapacityInMilliwattHours).

Untuk pengontrol game yang mendukung pelaporan baterai terperinci, Anda bisa mendapatkan ini dan informasi lebih lanjut tentang baterai, seperti yang dijelaskan dalam Dapatkan informasi baterai. Namun, sebagian besar pengontrol game tidak mendukung tingkat pelaporan baterai tersebut, dan sebaliknya menggunakan perangkat keras ber biaya 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 yang dihitung.

  • Persentase dari poin sebelumnya akan selalu menjadi salah satu dari berikut ini:

    • 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%.

Lihat juga