Eingabemethoden für SpieleInput practices for games

In diesem Thema werden Muster und Techniken für die effektive Verwendung von Eingabegeräten in universelle Windows-Plattform spielen (UWP) beschrieben.This topic describes patterns and techniques for effectively using input devices in Universal Windows Platform (UWP) games.

In diesem Thema lernen Sie Folgendes:By reading this topic, you'll learn:

  • Nachverfolgen von Spielern und den von ihnen aktuell verwendeten Eingabe- und Navigationsgerätenhow to track players and which input and navigation devices they're currently using
  • Erkennen von Tastenübergängen (von „Gedrückt“ zu „Losgelassen“ oder von „Losgelassen“ zu „Gedrückt“)how to detect button transitions (pressed-to-released, released-to-pressed)
  • Erkennen komplexer Tastenanordnungen mit einem einzelnen Testhow to detect complex button arrangements with a single test

Auswählen einer Eingabegeräte KlasseChoosing an input device class

Ihnen stehen viele verschiedene Arten von Eingabe-APIs zur Verfügung, z. b. " arcadebug", " Flightstick" und " Gamepad".There are many different types of input APIs available to you, such as ArcadeStick, FlightStick, and Gamepad. Wie entscheiden Sie, welche API für Ihr Spiel verwendet werden soll?How do you decide which API to use for your game?

Sie sollten auswählen, welche API Ihnen die am besten geeignete Eingabe für das Spiel liefert.You should choose whichever API gives you the most appropriate input for your game. Wenn Sie z. b. ein 2D-Plattformspiel erstellen, können Sie wahrscheinlich einfach die Gamepad -Klasse verwenden und sich nicht mit der zusätzlichen Funktionalität von anderen Klassen beschäftigen.For example, if you're making a 2D platform game, you can probably just use the Gamepad class and not bother with the extra functionality available via other classes. Dies würde das Spiel nur auf die Unterstützung von Gamepads beschränken und eine konsistente Schnittstelle bereitstellen, die über viele verschiedene Gamepads ohne zusätzlichen Code hinweg funktioniert.This would restrict the game to supporting gamepads only and provide a consistent interface that will work across many different gamepads with no need for additional code.

Andererseits können Sie für komplexe Flug-und Rennsimulationen alle rawgamecontroller -Objekte als Baseline auflisten, um sicherzustellen, dass Sie alle Nischen Geräte unterstützen, die von begeisterten Playern genutzt werden können, einschließlich Geräten wie separaten Pedalen oder Drosselungen, die noch von einem einzelnen Player verwendet werden.On the other hand, for complex flight and racing simulations, you might want to enumerate all of the RawGameController objects as a baseline to make sure they support any niche device that enthusiast players might have, including devices such as separate pedals or throttle that are still used by a single player.

Von dort aus können Sie die fromgamecontroller -Methode einer Eingabe Klasse, wie z . b. Gamepad. fromgamecontroller, verwenden, um festzustellen, ob jedes Gerät über eine allgemeinere Ansicht verfügt.From there, you can use an input class's FromGameController method, such as Gamepad.FromGameController, to see if each device has a more curated view. Wenn das Gerät z. b. auch ein Gamepadist, können Sie die Benutzeroberfläche der Schaltflächen Zuordnung anpassen, um dies widerzuspiegeln, und einige sinnvolle Standard Schaltflächen Zuordnungen bereitstellen, aus denen Sie auswählen können.For example, if the device is also a Gamepad, then you might want to adjust the button mapping UI to reflect that, and provide some sensible default button mappings to choose from. (Im Gegensatz dazu muss der Spieler die Gamepad-Eingaben manuell konfigurieren, wenn Sie nur rawgamecontrollerverwenden.)(This is in contrast to requiring the player to manually configure the gamepad inputs if you're only using RawGameController.)

Alternativ können Sie sich die Hersteller-ID (vid) und die Produkt-ID (PID) eines rawgamecontroller (mit hardwarevendorid bzw. hardwareproductid) ansehen und vorgeschlagene Schaltflächen Zuordnungen für beliebte Geräte bereitstellen, während Sie weiterhin mit unbekannten Geräten kompatibel sind, die in der Zukunft über manuelle Zuordnungen durch den Player auftreten.Alternatively, you can look at the vendor ID (VID) and product ID (PID) of a RawGameController (using HardwareVendorId and HardwareProductId, respectively) and provide suggested button mappings for popular devices while still remaining compatible with unknown devices that come out in the future via manual mappings by the player.

Verfolgen verbundener ControllerKeeping track of connected controllers

Obwohl jeder Controllertyp eine Liste verbundener Controller (z . b. Gamepad. Gamepads) enthält, empfiehlt es sich, eine eigene Liste von Controllern beizubehalten.While each controller type includes a list of connected controllers (such as Gamepad.Gamepads), it is a good idea to maintain your own list of controllers. Weitere Informationen finden Sie in der Gamepads-Liste (jeder Controllertyp weist einen entsprechend benannten Abschnitt zu einem eigenen Thema auf).See The gamepads list for more information (each controller type has a similarly named section on its own topic).

Was geschieht jedoch, wenn der Player den Controller entschließt oder einen neuen einfügt?However, what happens when the player unplugs their controller, or plugs in a new one? Sie müssen diese Ereignisse behandeln und die Liste entsprechend aktualisieren.You need to handle these events, and update your list accordingly. Weitere Informationen finden Sie unter Hinzufügen und Entfernen von Gamepads (auch hier hat jeder Controllertyp einen ähnlich benannten Abschnitt zu einem eigenen Thema).See Adding and removing gamepads for more information (again, each controller type has a similarly named section on its own topic).

Da die hinzugefügten und entfernten Ereignisse asynchron ausgelöst werden, erhalten Sie möglicherweise falsche Ergebnisse beim Umgang mit der Liste der Controller.Because the added and removed events are raised asynchronously, you could get incorrect results when dealing with your list of controllers. Daher sollten Sie immer dann, wenn Sie auf die Controller Liste zugreifen, eine Sperre dafür durchführen, dass jeweils nur ein Thread darauf zugreifen kann.Therefore, anytime you access your list of controllers, you should put a lock around it so that only one thread can access it at a time. Dies kann mit dem Concurrency Runtime, insbesondere der CRITICAL_SECTION- Klasse, in ** < ppl. h > **erfolgen.This can be done with the Concurrency Runtime, specifically the critical_section class, in <ppl.h>.

Ein weiterer Aspekt ist, dass die Liste der verbundenen Controller anfänglich leer ist und ein zweites oder zwei zum Auffüllen benötigt.Another thing to think about is that the list of connected controllers will initially be empty, and takes a second or two to populate. Wenn Sie also nur das aktuelle Gamepad in der Start-Methode zuweisen, ist es null!So if you only assign the current gamepad in the start method, it will be null!

Um dies zu beheben, sollten Sie über eine Methode verfügen, die das Haupt Gamepad "aktualisiert" (in einem Einzelspielerspiel; Multiplayerspiele benötigen anspruchsvollere Lösungen).To rectify this, you should have a method that "refreshes" the main gamepad (in a single-player game; multiplayer games will require more sophisticated solutions). Sie sollten diese Methode dann sowohl in Ihrem Controller hinzugefügten Controller als auch in der Update-Methode als von einem Controller entfernte Ereignishandler oder in der Update-MethodeYou should then call this method in both your controller added and controller removed event handlers, or in your update method.

Die folgende Methode gibt einfach das erste Gamepad in der Liste zurück (oder nullptr , wenn die Liste leer ist).The following method simply returns the first gamepad in the list (or nullptr if the list is empty). Dann müssen Sie sich nur noch einmal auf nullptr überprüfen, wenn Sie mit dem Controller etwas tun.Then you just need to remember to check for nullptr anytime you do anything with the controller. Es liegt an Ihnen, ob Sie das Spiel blockieren möchten, wenn kein Controller verbunden ist (z. b. durch Anhalten des Spiels), oder wenn Sie einfach das Design fortsetzen möchten, während die Eingabe ignoriert wird.It's up to you whether you want to block gameplay when there's no controller connected (for example, by pausing the game) or simply have gameplay continue, while ignoring 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;
}

Im folgenden finden Sie ein Beispiel für die Handhabung von Eingaben von einem Gamepad:Putting it all together, here is an example of how to handle input from a 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);
    }
}

Nachverfolgen von Benutzern und ihren GerätenTracking users and their devices

Alle Eingabegeräte sind einem Benutzer zugeordnet, damit seine Identität mit seinem Spiel, seinen Erfolgen, geänderten Einstellungen und anderen Aktivitäten verknüpft werden kann.All input devices are associated with a User so that their identity can be linked to their gameplay, achievements, settings changes, and other activities. Benutzer können sich bei der Ausführung anmelden oder abmelden, und es ist üblich, dass sich ein anderer Benutzer auf einem Eingabegerät anmeldet, das mit dem System verbunden bleibt, nachdem sich der vorherige Benutzer abgemeldet hat. Wenn ein Benutzer sich anmeldet, wird das Ereignis igamecontroller. userchanged ausgelöst.Users can sign in or sign out at will, and it's common for a different user to sign in on an input device that remains connected to the system after the previous user has signed out. When a user signs in or out, the IGameController.UserChanged event is raised. Sie können einen Ereignishandler für dieses Ereignis registrieren, um Spieler und die von ihnen verwendeten Geräte nachzuverfolgen.You can register an event handler for this event to keep track of players and the devices they're using.

Die Benutzeridentität ist auch die Art und Weise, in der ein Eingabegerät dem entsprechenden UI-Navigations Controllerzugeordnet ist.User identity is also the way that an input device is associated with its corresponding UI navigation controller.

Aus diesen Gründen sollte die Eingabe von Player nachverfolgt und mit der User -Eigenschaft der Device-Klasse (geerbt von der igamecontroller -Schnittstelle) korreliert werden.For these reasons, player input should be tracked and correlated with the User property of the device class (inherited from the IGameController interface).

Das usergamepadplpyinguwp -Beispiel veranschaulicht, wie Sie die Benutzer und die von Ihnen verwendeten Geräte nachverfolgen können.The UserGamepadPairingUWP sample demonstrates how you can keep track of users and the devices they're using.

Erkennen von TastenübergängenDetecting button transitions

In einigen Fällen möchten Sie wissen, wann eine Taste zuerst gedrückt oder losgelassen wird, also genau, wann der Zustand der Taste von „Losgelassen“ in „Gedrückt“ oder von „Gedrückt“ in „Losgelassen“ übergegangen ist.Sometimes you want to know when a button is first pressed or released; that is, precisely when the button state transitions from released to pressed or from pressed to released. Um dies zu ermitteln und herauszufinden, was sich geändert hat, müssen Sie den vorherigen Geräte-Ablesewert beibehalten und mit dem aktuellen Ablesewert vergleichen.To determine this, you need to remember the previous device reading and compare the current reading against it to see what's changed.

Im folgenden Beispiel wird eine grundlegende Vorgehensweise zum Speichern der vorherigen Lektüre veranschaulicht. Gamepads werden hier angezeigt, aber die Prinzipien sind für den Faden Strich, das Rennrad und die anderen Eingabegeräte Typen identisch.The following example demonstrates a basic approach for remembering the previous reading; gamepads are shown here, but the principles are the same for arcade stick, racing wheel, and the other input device types.

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)
}

Als Erstes verschiebt Game::Loop den vorhandenen Wert von newReading (den Gamepad-Ablesewert aus der vorherigen Schleifeniteration) in oldReading und fügt dann einen neuen Gamepad-Ablesewert für die aktuelle Iteration in newReading ein.Before doing anything else, Game::Loop moves the existing value of newReading (the gamepad reading from the previous loop iteration) into oldReading, then fills newReading with a fresh gamepad reading for the current iteration. Mithilfe dieser Informationen können Sie den Übergang von Tasten erkennen.This gives you the information you need to detect button transitions.

Im folgenden Beispiel wird ein grundlegender Ansatz zum Erkennen von Schaltflächen Übergängen veranschaulicht:The following example demonstrates a basic approach for detecting button transitions:

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;
}

Diese beiden Funktionen leiten zuerst den booleschen Zustand der Schaltflächen Auswahl von newReading und ab und oldReading führen dann eine boolesche Logik aus, um zu bestimmen, ob der Ziel Übergang aufgetreten ist.These two functions first derive the Boolean state of the button selection from newReading and oldReading, then perform Boolean logic to determine whether the target transition has occurred. Diese Funktionen geben nur true zurück, wenn der neue Ablesewert den Zielzustand enthält („Gedrückt“ bzw. „Losgelassen“) und der alte Ablesewert nicht auch den Zielzustand enthält. Andernfalls geben sie false zurück.These functions return true only if the new reading contains the target state (pressed or released, respectively) and the old reading does not also contain the target state; otherwise, they return false.

Erkennen komplexer TastenanordnungenDetecting complex button arrangements

Jede Schaltfläche eines Eingabe Geräts bietet ein digitales Lesematerial, das angibt, ob es gedrückt oder freigegeben wird (nach oben).Each button of an input device provides a digital reading that indicates whether it's pressed (down) or released (up). Aus Effizienzgründen werden die Ablesewerte der Tasten nicht als einzelne boolesche Werte dargestellt, sondern in Bitfeldern zusammengefasst, die durch gerätespezifische Enumerationen wie GamepadButtons dargestellt werden.For efficiency, button readings aren't represented as individual boolean values; instead, they're all packed into bitfields represented by device-specific enumerations such as GamepadButtons. Zum Lesen bestimmter Tastenwerte wird eine bitweise Maskierung verwendet, um die für Sie relevanten Werte zu isolieren.To read specific buttons, bitwise masking is used to isolate the values that you're interested in. Eine Schaltfläche wird gedrückt (unten), wenn das entsprechende Bit festgelegt ist. Andernfalls wird Sie freigegeben (up).A button is pressed (down) when its corresponding bit is set; otherwise, it's released (up).

Erinnern Sie sich, wie einzelne Schaltflächen als gedrückt oder freigegeben festgelegt werden. Gamepads werden hier angezeigt, aber die Prinzipien sind für den Faden Strich, das Rennrad und die anderen Eingabegeräte Typen identisch.Recall how single buttons are determined to be pressed or released; gamepads are shown here, but the principles are the same for arcade stick, racing wheel, and the other input device types.

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).
}

Wie Sie sehen können, ist die Bestimmung des Zustands einer einzelnen Schaltfläche geradlinig, aber manchmal möchten Sie möglicherweise feststellen, ob mehrere Schaltflächen gedrückt oder freigegeben werden, oder ob eine Reihe von Schaltflächen auf eine bestimmte Art und Weise angeordnet ist — , einige nicht.As you can see, determining the state of a single button is straight-forward, but sometimes you might want to determine whether multiple buttons are pressed or released, or if a set of buttons are arranged in a particular way—some pressed, some not. Das Testen mehrerer Schaltflächen ist komplexer als das Testen einzelner Schaltflächen — , vor allem mit dem Potenzial des — gemischtem Schaltflächen Zustands, aber es gibt eine einfache Formel für diese Tests, die für einzelne und mehrere Schaltflächen Tests gleichermaßen gilt.Testing multiple buttons is more complex than testing single buttons—especially with the potential of mixed button state—but there's a simple formula for these tests that applies to single and multiple button tests alike.

Im folgenden Beispiel wird bestimmt, ob die Gamepad-Schaltflächen A und B gedrückt werden:The following example determines whether gamepad buttons A and B are both pressed:

if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both pressed.
}

Im folgenden Beispiel wird bestimmt, ob die Gamepad-Schaltflächen A und B freigegeben werden:The following example determines whether gamepad buttons A and B are both released:

if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both released (not pressed).
}

Im folgenden Beispiel wird bestimmt, ob Gamepad Button A gedrückt wird, während Schaltfläche B losgelassen wird:The following example determines whether gamepad button A is pressed while button B is released:

if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A button is pressed and the B button is released (B is not pressed).
}

Die Formel, die in allen fünf Beispielen verwendet wird, ist wie folgt aufgebaut: Die Anordnung der zu überprüfenden Tasten wird durch den Ausdruck auf der linken Seite des Gleichheitsoperators angegeben, während die Tasten, die berücksichtigt werden sollen, durch den Maskierungsausdruck auf der rechten Seite ausgewählt werden.The formula that all five of these examples have in common is that the arrangement of buttons to be tested for is specified by the expression on the left-hand side of the equality operator while the buttons to be considered are selected by the masking expression on the right-hand side.

Im folgenden Beispiel wird diese Formel deutlicher veranschaulicht, indem das vorherige Beispiel umgeschrieben wird:The following example demonstrates this formula more clearly by rewriting the previous example:

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).
}

Die Formel kann zum Testen einer beliebigen Anzahl von Tasten in einer beliebigen Anordnung ihrer Zustände verwendet werden.This formula can be applied to test any number of buttons in any arrangement of their states.

Den Zustand des Akkus erhaltenGet the state of the battery

Für alle Spiele Controller, die die igamecontrollerbatteryinfo -Schnittstelle implementieren, können Sie trygetbatteryreport für die Controller Instanz aufrufen, um ein batteryreport -Objekt zu erhalten, das Informationen über den Akku im Controller bereitstellt.For any game controller that implements the IGameControllerBatteryInfo interface, you can call TryGetBatteryReport on the controller instance to get a BatteryReport object that provides information about the battery in the controller. Sie können Eigenschaften wie die Geschwindigkeit, mit der der Akku belastet wird (chargerateinmilliwatt), die geschätzte Energiekapazität eines neuen Akkus (designcapacityinmilliwatthours) und die vollständig berechnete Energiekapazität des aktuellen Akkus (fullchargecapacityinmilliwatthours) erhalten.You can get properties like the rate that the battery is charging (ChargeRateInMilliwatts), the estimated energy capacity of a new battery (DesignCapacityInMilliwattHours), and the fully-charged energy capacity of the current battery (FullChargeCapacityInMilliwattHours).

Für Game Controller, die eine ausführliche Akku Berichterstattung unterstützen, können Sie diese und weitere Informationen zum Akku erhalten, wie in " Get Akku Information" beschrieben.For game controllers that support detailed battery reporting, you can get this and more information about the battery, as detailed in Get battery information. Allerdings unterstützen die meisten Spiele Controller diese Ebene der Akku Berichterstattung nicht, sondern verwenden stattdessen kostengünstige Hardware.However, most game controllers don't support that level of battery reporting, and instead use low-cost hardware. Für diese Controller müssen Sie die folgenden Aspekte berücksichtigen:For these controllers, you'll need to keep the following considerations in mind:

  • Chargerateinmilliwatt und designcapacityinmilliwatthours sind immer null.ChargeRateInMilliwatts and DesignCapacityInMilliwattHours will always be NULL.

  • Sie können den Akku Prozentsatz durch Berechnen von restingcapacityinmilliwatthours / fullchargecapacityinmilliwatthourserhalten.You can get the battery percentage by calculating RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours. Die Werte dieser Eigenschaften sollten ignoriert werden, und es sollte nur der berechnete Prozentsatz behandelt werden.You should ignore the values of these properties and only deal with the calculated percentage.

  • Der Prozentsatz aus dem vorherigen Aufzählungs Punkt ist immer eine der folgenden:The percentage from the previous bullet point will always be one of the following:

    • 100% (vollständig)100% (Full)
    • 70% (Mittel)70% (Medium)
    • 40% (niedrig)40% (Low)
    • 10% (kritisch)10% (Critical)

Wenn Ihr Code Aktionen (z. b. das Zeichnen der Benutzeroberfläche) durchführt, die auf dem Prozentsatz der verbleibenden Akku Lebensdauer basieren, stellen Sie sicher, dass die Werte den obigenIf your code performs some action (like drawing UI) based on the percentage of battery life remaining, make sure that it conforms to the values above. Wenn Sie z. b. den Player warnen möchten, wenn der Akku des Controllers niedrig ist, gehen Sie so vor, wenn er 10% erreicht.For example, if you want to warn the player when the controller's battery is low, do so when it reaches 10%.

Weitere InformationenSee also