定义主游戏对象Define the main game object

备注

本主题是 使用 DirectX 教程系列 (UWP) 游戏创建简单通用 Windows 平台 的一部分。This topic is part of the Create a simple Universal Windows Platform (UWP) game with DirectX tutorial series. 该链接上的主题设置了序列的上下文。The topic at that link sets the context for the series.

完成示例游戏的基本框架并实现了用于处理高级用户和系统行为的状态机后,您将需要检查将示例游戏变成游戏的规则和机制。Once you've laid out the basic framework of the sample game, and implemented a state machine that handles the high-level user and system behaviors, you'll want to examine the rules and mechanics that turn the sample game into a game. 让我们看看示例游戏主对象的详细信息,以及如何将游戏规则转换为与游戏世界的交互。Let's look at the details of the sample game's main object, and how to translate game rules into interactions with the game world.

目标Objectives

  • 了解如何应用基本开发技术来实现 UWP DirectX 游戏的游戏规则和机制。Learn how to apply basic development techniques to implement game rules and mechanics for a UWP DirectX game.

主要游戏对象Main game object

Simple3DGameDX 示例游戏中, Simple3DGame 是主要游戏对象类。In the Simple3DGameDX sample game, Simple3DGame is the main game object class. Simple3DGame的实例通过应用:: Load方法间接构造。An instance of Simple3DGame is constructed, indirectly, via the App::Load method.

下面是 Simple3DGame 类的一些功能。Here are some of the features of the Simple3DGame class.

  • 包含游戏逻辑的实现。Contains implementation of the gameplay logic.
  • 包含用于传达这些详细信息的方法。Contains methods that communicate these details.
    • 游戏状态更改为应用框架中定义的状态机。Changes in the game state to the state machine defined in the app framework.
    • 从应用到游戏对象本身的游戏状态更改。Changes in the game state from the app to the game object itself.
    • 有关更新游戏 UI (覆盖和打印头的详细信息,请 (dynamics) ) 、动画和物理学。Details for updating the game's UI (overlay and heads-up display), animations, and physics (the dynamics).

    备注

    更新图形由 GameRenderer 类处理,该类包含用于获取和使用该游戏所使用的图形设备资源的方法。Updating of graphics is handled by the GameRenderer class, which contains methods to obtain and use graphics device resources used by the game. 有关详细信息,请参阅 呈现框架 I:要呈现的简介For more info, see Rendering framework I: Intro to rendering.

  • 用作定义游戏会话、级别或生存期的数据的容器,具体取决于你在高级别定义游戏的方式。Serves as a container for the data that defines a game session, level, or lifetime, depending on how you define your game at a high level. 在这种情况下,游戏状态数据适用于游戏的生存期,在用户启动游戏时初始化一次。In this case, the game state data is for the lifetime of the game, and is initialized one time when a user launches the game.

若要查看由此类定义的方法和数据,请参阅下面 的 Simple3DGame 类To view the methods and data defined by this class, see The Simple3DGame class below.

初始化并启动游戏Initialize and start the game

当玩家启动游戏时,游戏对象必须初始化其状态,创建和添加覆盖层,设置用于跟踪玩家成绩的变量,并实例化将用于构建级别的对象。When a player starts the game, the game object must initialize its state, create and add the overlay, set the variables that track the player's performance, and instantiate the objects that it will use to build the levels. 在此示例中,此操作在应用:: Load中创建GameMain实例时完成。In this sample, this is done when the GameMain instance is created in App::Load.

类型为 Simple3DGame的游戏对象在 GameMain:: GameMain 构造函数中创建。The game object, of type Simple3DGame, is created in the GameMain::GameMain constructor. 然后,在GameMain:: ConstructInBackground 发出的:: (从GameMain:: GameMain调用)期间使用Simple3DGame:: Initialize方法对其进行初始化。It's then initialized using the Simple3DGame::Initialize method during the GameMain::ConstructInBackground fire-and-forget coroutine, which is called from GameMain::GameMain.

Simple3DGame:: Initialize 方法The Simple3DGame::Initialize method

示例游戏在游戏对象中设置这些组件。The sample game sets up these components in the game object.

  • 创建新的音频播放对象。A new audio playback object is created.
  • 创建游戏的图形基元的数组,包括级别基元、弹药和障碍的数组。Arrays for the game's graphic primitives are created, including arrays for the level primitives, ammo, and obstacles.
  • 创建用于保存游戏状态数据的位置,名为游戏,并放入由 ApplicationData::Current 指定的应用数据设置存储位置。A location for saving game state data is created, named Game, and placed in the app data settings storage location specified by ApplicationData::Current.
  • 创建游戏计时器和初始游戏内重叠位图。A game timer and the initial in-game overlay bitmap are created.
  • 使用一组特定的视图和投影参数创建新的相机。A new camera is created with a specific set of view and projection parameters.
  • 输入设备(控制器)设置为与相机相同的俯仰和偏航,以使玩家在起始控制位置和相机位置之间具有 1 对 1 的对应性。The input device (the controller) is set to the same starting pitch and yaw as the camera, so the player has a 1-to-1 correspondence between the starting control position and the camera position.
  • 创建玩家对象并设置为活动。The player object is created and set to active. 我们使用球体对象检测玩家与墙壁和障碍的距离,并使相机远离可能中断浸入式的位置。We use a sphere object to detect the player's proximity to walls and obstacles and to keep the camera from getting placed in a position that might break immersion.
  • 创建游戏世界基元。The game world primitive is created.
  • 创建圆柱障碍。The cylinder obstacles are created.
  • 创建目标(Face 对象)并编号。The targets (Face objects) are created and numbered.
  • 创建弹药球体。The ammo spheres are created.
  • 创建级别。The levels are created.
  • 加载高分。The high score is loaded.
  • 加载任何之前保存的游戏状态。Any prior saved game state is loaded.

游戏现在提供 — 了世界、玩家、障碍、目标和 ammo 球的所有关键组件的实例。The game now has instances of all the key components—the world, the player, the obstacles, the targets, and the ammo spheres. 还有各级别的实例,表示上述所有组件的配置以及每个特定级别的行为。It also has instances of the levels, which represent configurations of all of the above components and their behaviors for each specific level. 现在让我们看看游戏如何生成级别。Now let's see how the game builds the levels.

构建和加载游戏级别Build and load game levels

Level[N].h/.cpp 示例解决方案的 GameLevels 文件夹中找到的文件中,级别构造的大部分繁重的提升都是如此。Most of the heavy lifting for the level construction is done in the Level[N].h/.cpp files found in the GameLevels folder of the sample solution. 因为它侧重于一个非常具体的实现,所以我们不会在此涵盖它们。Because it focuses on a very specific implementation, we won't be covering them here. 重要的是,每个级别的代码都作为一个单独的 级别 [N] 对象运行。The important thing is that the code for each level is run as a separate Level[N] object. 如果您想要扩展游戏,则可以创建一个级别为 [N] 的对象,该对象使用分配的数字作为参数,并随机放置障碍和目标。If you'd like to extend the game, you can create a Level[N] object that takes an assigned number as a parameter and randomly places the obstacles and targets. 或者,你可以让它从资源文件甚至 internet 加载级别的配置数据。Or, you can have it load level configuration data from a resource file, or even the internet.

定义游戏Define the gameplay

此时,我们已经有了开发该游戏所需的所有组件。At this point, we have all the components we need to develop the game. 已在内存中使用基元构造了级别,并已准备好让播放机开始与进行交互。The levels have been constructed in memory from the primitives, and are ready for the player to start interacting with.

最佳游戏会立即做出反应,并提供即时反馈。The best games react instantly to player input, and provide immediate feedback. 这适用于任何类型的游戏,从 twitch、实时的第一人称 shooters 到精心的、基于车的战略游戏。This is true for any type of a game, from twitch-action, real-time first-person shooters to thoughtful, turn-based strategy games.

Simple3DGame:: RunGame 方法The Simple3DGame::RunGame method

当游戏级别正在进行时,游戏处于 Dynamics 状态。While a game level is in progress, the game is in the Dynamics state.

GameMain:: Update 是每帧更新一次应用程序状态的主要更新循环,如下所示。GameMain::Update is the main update loop that updates the application state once per frame, as shown below. 如果游戏处于Dynamics状态,则 update 循环将调用Simple3DGame:: RunGame方法来处理工作。The update loop calls the Simple3DGame::RunGame method to handle the work if the game is in the Dynamics state.

// Updates the application state once per frame.
void GameMain::Update()
{
    // The controller object has its own update loop.
    m_controller->Update();

    switch (m_updateState)
    {
    ...
    case UpdateEngineState::Dynamics:
        if (m_controller->IsPauseRequested())
        {
            ...
        }
        else
        {
            // When the player is playing, work is done by Simple3DGame::RunGame.
            GameState runState = m_game->RunGame();
            switch (runState)
            {
                ...

Simple3DGame:: RunGame 用于处理定义游戏循环当前迭代的游戏播放当前状态的数据集。Simple3DGame::RunGame handles the set of data that defines the current state of the game play for the current iteration of the game loop.

下面是 Simple3DGame:: RunGame中的游戏流逻辑。Here's the game flow logic in Simple3DGame::RunGame.

  • 此方法将更新在数秒内倒计时的计时器,直到级别完成,并进行测试以查看级别的时间是否已到期。The method updates the timer that counts down the seconds until the level is completed, and tests to see whether the level's time has expired. 这是 — 当时间用完时游戏的规则之一,如果不是所有目标都已被拍摄,则会对其进行游戏。This is one of the rules of the game—when time runs out, if not all the targets have been shot, then it's game over.
  • 如果已用完时间,则方法将设置 TimeExpired 游戏状态,并返回到上一代码中的 Update 方法。If time has run out, then the method sets the TimeExpired game state, and returns to the Update method in the previous code.
  • 如果持续时间仍然存在,则会轮询移动外观控制器以便更新到相机位置;具体而言,就是指从照相机平面投影到的角度的更新 (播放机查找) ,以及从控制器最后轮询后角度已移动的距离。If time remains, then the move-look controller is polled for an update to the camera position; specifically, an update to the angle of the view normal projecting from the camera plane (where the player is looking), and the distance that angle has moved since the controller was polled last.
  • 将基于来自移动观看控制器的新数据更新相机。The camera is updated based on the new data from the move-look controller.
  • 将更新游戏世界中独立于玩家控制的对象的动态或动画和行为。The dynamics, or the animations and behaviors of objects in the game world independent of player control, are updated. 在此示例游戏中,将调用 Simple3DGame:: UpdateDynamics 方法来更新已激发的 ammo 球体的运动、支柱障碍的动画和目标的移动。In this sample game, the Simple3DGame::UpdateDynamics method is called to update the motion of the ammo spheres that have been fired, the animation of the pillar obstacles and the movement of the targets. 有关详细信息,请参阅 更新游戏世界For more information, see Update the game world.
  • 方法将检查是否已满足某个级别的成功完成条件。The method checks to see whether the criteria for the successful completion of a level have been met. 如果是,则它将为级别完成评分,并检查这是否为最后一个级别 (6) 。If so, it finalizes the score for the level, and checks to see whether this is the last level (of 6). 如果它是最后一个级别,则方法返回 GameState:: GameComplete 游戏状态;否则,它将返回 GameState:: LevelComplete 游戏状态。If it's the last level, then the method returns the GameState::GameComplete game state; otherwise, it returns the GameState::LevelComplete game state.
  • 如果该级别未完成,则方法将游戏状态设置为 GameState:: Active,并返回。If the level isn't complete, then the method sets the game state to GameState::Active, and returns.

更新游戏世界Update the game world

在此示例中,当游戏运行时, Simple3DGame:: UpdateDynamics 方法从 Simple3DGame:: RunGame 方法调用,该 (方法从 GameMain:: update) 调用,以更新游戏场景中呈现的对象。In this sample, when the game is running, the Simple3DGame::UpdateDynamics method is called from the Simple3DGame::RunGame method (which is called from GameMain::Update) to update objects that are rendered in a game scene.

诸如 UpdateDynamics 之类的循环会调用用于设置游戏世界的任何方法,与播放机输入无关,以创建沉浸式游戏体验并使其处于活动状态。A loop such as UpdateDynamics calls any methods that are used to set the game world in motion, independent of the player input, to create an immersive game experience and make the level come alive. 这包括需要呈现的图形,并且即使在没有播放机输入的情况下,也可以运行动画循环来提供有关动态世界的信息。This includes graphics that needs to be rendered, and running animation loops to bring about a dynamic world even when there's no player input. 在游戏中,这可能包括风的 swaying、波形 cresting 沿支撑线路、机械吸烟,以及外部庞然大物拉伸和移动。In your game, that could include trees swaying in the wind, waves cresting along shore lines, machinery smoking, and alien monsters stretching and moving around. 还包括对象之间的交互,包括玩家范围和游戏世界之间的冲突或者弹药与障碍物和目标之间的冲突。It also encompasses the interaction between objects, including collisions between the player sphere and the world, or between the ammo and the obstacles and targets.

除非游戏特别暂停,否则游戏循环应继续更新游戏世界;无论是基于游戏逻辑、物理算法还是只是普通随机。Except when the game is specifically paused, the game loop should continue updating the game world; whether that's based on game logic, physical algorithms, or whether it's just plain random.

在此示例游戏中,这一原则称为 dynamics,它包括支柱障碍的上升和下降,并且 ammo 球的运动和物理行为在激发和运动时出现。In the sample game, this principle is called dynamics, and it encompasses the rise and fall of the pillar obstacles, and the motion and physical behaviors of the ammo spheres as they are fired and in motion.

Simple3DGame:: UpdateDynamics 方法The Simple3DGame::UpdateDynamics method

此方法处理这四组计算。This method deals with these four sets of computations.

  • 游戏世界中触发的弹药范围的位置。The positions of the fired ammo spheres in the world.
  • 立柱障碍物的动画。The animation of the pillar obstacles.
  • 玩家与游戏世界界限的相交处。The intersection of the player and the world boundaries.
  • 弹药球体与障碍物、目标、其他弹药球体和游戏世界的碰撞。The collisions of the ammo spheres with the obstacles, the targets, other ammo spheres, and the world.

障碍的动画发生在 动画 .h 和 .cpp 源代码文件中定义的循环中。The animation of the obstacles takes place in a loop defined in the Animate.h/.cpp source code files. Ammo 和任何冲突的行为由代码中提供的简化物理学算法定义,并由游戏世界的一组全局常量进行参数化,包括重力和材料属性。The behavior of the ammo and any collisions are defined by simplified physics algorithms, supplied in the code and parameterized by a set of global constants for the game world, including gravity and material properties. 这都在游戏世界坐标空间中计算。This is all computed in the game world coordinate space.

查看流Review the flow

现在我们已更新场景中的所有对象,并计算出冲突,我们需要使用此信息来绘制相应的视觉对象更改。Now that we've updated all of the objects in the scene, and calculated any collisions, we need to use this info to draw the corresponding visual changes.

GameMain:: Update 完成游戏循环的当前迭代后,示例立即调用 GameRenderer:: Render 以获取更新的对象数据,并生成新的场景以呈现给播放机,如下所示。After GameMain::Update has completed the current iteration of the game loop, the sample immediately calls GameRenderer::Render to take the updated object data and generate a new scene to present to the player, as shown below.

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible)
        {
            switch (m_updateState)
            {
            case UpdateEngineState::Deactivated:
            case UpdateEngineState::TooSmall:
                ...
                // Otherwise, fall through and do normal processing to perform rendering.
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
                    CoreProcessEventsOption::ProcessAllIfPresent);
                // GameMain::Update calls Simple3DGame::RunGame. If game is in Dynamics
                // state, uses Simple3DGame::UpdateDynamics to update game world.
                Update();
                // Render is called immediately after the Update loop.
                m_renderer->Render();
                m_deviceResources->Present();
                m_renderNeeded = false;
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(
                CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
    m_game->OnSuspending();  // Exiting due to window close, so save state.
}

渲染游戏世界的图形Render the game world's graphics

建议经常更新游戏中的图形,理想情况下,主要游戏循环会循环访问。We recommend that the graphics in a game update often, ideally exactly as often as the main game loop iterates. 循环循环访问时,游戏世界的状态将更新,有或没有播放机输入。As the loop iterates, the game world's state is updated, with or without player input. 这样,计算出的动画和行为就能顺利地显示。This allows the calculated animations and behaviors to be displayed smoothly. 假设我们有一个只在玩家按下按钮时移动的简单水印场景。Imagine if we had a simple scene of water that moved only when the player pressed a button. 这并不现实;一个不错的游戏看起来平滑且流畅。That wouldn't be realistic; a good game looks smooth and fluid all the time.

再次调用 GameMain:: Run中的示例游戏循环,如上文所示。Recall the sample game's loop as shown above in GameMain::Run. 如果游戏的主窗口可见,且未进行快照或停用,则游戏将继续更新并呈现该更新的结果。If the game's main window is visible, and isn't snapped or deactivated, then the game continues to update and render the results of that update. 我们下一次检查的 GameRenderer:: Render 方法呈现该状态的表示形式。The GameRenderer::Render method we examine next renders a representation of that state. 这是在调用 GameMain:: update后立即完成的,其中包括 Simple3DGame:: RunGame 以更新状态,如前一部分中所述。This is done immediately after a call to GameMain::Update, which includes Simple3DGame::RunGame to update states, as discussed in the previous section.

GameRenderer:: Render 绘制三维世界的投影,然后在其顶部绘制 Direct2D 叠加。GameRenderer::Render draws the projection of the 3D world, and then draws the Direct2D overlay on top of it. 在完成之后,它将使用合并的缓冲区提供最后的交换链进行显示。When completed, it presents the final swap chain with the combined buffers for display.

备注

对于示例游戏的 Direct2D 叠加有两个状态 — ,游戏显示了包含 "暂停" 菜单的位图的游戏信息覆盖,另一个游戏显示了十字线和触摸屏移动外观控制器的矩形。There are two states for the sample game's Direct2D overlay—one where the game displays the game info overlay that contains the bitmap for the pause menu, and one where the game displays the crosshairs along with the rectangles for the touchscreen move-look controller. 得分文本在两个状态中绘制。The score text is drawn in both states. 有关详细信息,请参阅呈现框架 I:呈现简介For more information, see Rendering framework I: Intro to rendering.

GameRenderer:: Render 方法The GameRenderer::Render method

void GameRenderer::Render()
{
    bool stereoEnabled{ m_deviceResources->GetStereoState() };

    auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
    auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };

    ...
        if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
        {
            // This section is only used after the game state has been initialized and all device
            // resources needed for the game have been created and associated with the game objects.
            ...
            for (auto&& object : m_game->RenderObjects())
            {
                object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
            }
        }

        d3dContext->BeginEventInt(L"D2D BeginDraw", 1);
        d2dContext->BeginDraw();

        // To handle the swapchain being pre-rotated, set the D2D transformation to include it.
        d2dContext->SetTransform(m_deviceResources->GetOrientationTransform2D());

        if (m_game != nullptr && m_gameResourcesLoaded)
        {
            // This is only used after the game state has been initialized.
            m_gameHud.Render(m_game);
        }

        if (m_gameInfoOverlay.Visible())
        {
            d2dContext->DrawBitmap(
                m_gameInfoOverlay.Bitmap(),
                m_gameInfoOverlayRect
                );
        }
        ...
    }
}

Simple3DGame 类The Simple3DGame class

这些是由 Simple3DGame 类定义的方法和数据成员。These are the methods and data members that are defined by the Simple3DGame class.

成员函数Member functions

Simple3DGame定义的公共成员函数包括下面的函数。Public member functions defined by Simple3DGame include the ones below.

  • 初始化Initialize. 设置全局变量的起始值并初始化游戏对象。Sets the starting values of the global variables, and initializes the game objects. 这将在 " 初始化和启动游戏" 部分中介绍。This is covered in the Initialize and start the game section.
  • LoadGameLoadGame. 初始化新级别并开始加载它。Initializes a new level, and starts loading it.
  • LoadLevelAsyncLoadLevelAsync. 用于初始化级别的协同程序,然后调用呈现器上的另一个协同程序以加载特定于设备的级别资源。A coroutine that initializes the level, and then invokes another coroutine on the renderer to load the device-specific level resources. 该方法在单独的线程中运行;因此,只可从该线程调用 ID3D11Device 方法(相对于 ID3D11DeviceContext 方法)。This method runs in a separate thread; as a result, only ID3D11Device methods (as opposed to ID3D11DeviceContext methods) can be called from this thread. 任何设备上下文方法都在 FinalizeLoadLevel 方法中调用。Any device context methods are called in the FinalizeLoadLevel method. 如果你不熟悉异步编程,请参阅 c + +/WinRT 的并发和异步操作If you're new to asynchronous programming, then see Concurrency and asynchronous operations with C++/WinRT.
  • FinalizeLoadLevelFinalizeLoadLevel. 完成需要在主线程上进行的关卡加载所需的任何工作。Completes any work for level loading that needs to be done on the main thread. 这包括对 Direct3D 11 设备上下文 (ID3D11DeviceContext) 方法的任何调用。This includes any calls to Direct3D 11 device context (ID3D11DeviceContext) methods.
  • StartLevelStartLevel. 启动新级别的游戏。Starts the gameplay for a new level.
  • PauseGamePauseGame. 暂停游戏。Pauses the game.
  • RunGameRunGame. 运行游戏循环的迭代。Runs an iteration of the game loop. 如果游戏状态为 Active,则游戏循环的每次迭代都将从 App::Update 中调用它一次。It's called from App::Update one time every iteration of the game loop if the game state is Active.
  • OnSuspendingOnResumingOnSuspending and OnResuming. 分别暂停/继续游戏音频。Suspend/resume the game's audio, respectively.

下面是私有成员函数。Here are the private member functions.

  • LoadSavedStateSaveStateLoadSavedState and SaveState. 分别加载/保存游戏的当前状态。Load/save the current state of the game, respectively.
  • LoadHighScoreSaveHighScoreLoadHighScore and SaveHighScore. 分别在游戏中加载/保存高分。Load/save the high score across games, respectively.
  • InitializeAmmoInitializeAmmo. 在每一轮开始时,将用作弹药的各范围对象的状态重置回其初始状态。Resets the state of each sphere object used as ammunition back to its original state for the beginning of each round.
  • UpdateDynamicsUpdateDynamics. 这是一个重要的方法,因为它会根据固定动画例程、物理学和控件输入更新所有游戏对象。This is an important method because it updates all the game objects based on canned animation routines, physics, and control input. 这是定义游戏的交互性的核心。This is the heart of the interactivity that defines the game. 这会在 " 更新游戏" 一节中介绍。This is covered in the Update the game world section.

其他公共方法是属性访问器,可将游戏和特定于覆盖的信息返回到应用框架以便显示。The other public methods are property accessor that return gameplay- and overlay-specific information to the app framework for display.

数据成员Data members

游戏循环运行时,将更新这些对象。These objects are updated as the game loop runs.

  • MoveLookController 对象。MoveLookController object. 表示播放机输入。Represents the player input. 有关详细信息,请参阅添加控件For more information, see Adding controls.
  • GameRenderer 对象。GameRenderer object. 表示一个 Direct3D 11 呈现器,该呈现器处理所有设备特定的对象及其呈现。Represents a Direct3D 11 renderer, which handles all the device-specific objects and their rendering. 有关详细信息,请参阅 渲染框架 IFor more information, see Rendering framework I.
  • 音频 对象。Audio object. 控制游戏的音频播放。Controls the audio playback for the game. 有关详细信息,请参阅 添加声音For more information, see Adding sound.

游戏变量的其余部分包含基元的列表及其各自的游戏数量,游戏播放特定的数据和约束。The rest of the game variables contain the lists of the primitives, and their respective in-game amounts, and game play specific data and constraints.

后续步骤Next steps

我们尚未讨论实际的渲染引擎 — 如何在屏幕上转换对更新的基元的 渲染 方法的调用。We have yet to talk about the actual rendering engine—how calls to the Render methods on the updated primitives get turned into pixels on your screen. 以下两部分介绍了这两个方面 — :呈现框架 I:渲染渲染框架 II:游戏渲染Those aspects are covered in two parts—Rendering framework I: Intro to rendering and Rendering framework II: Game rendering. 如果对播放机控件更新游戏状态的方式更感兴趣,请参阅 添加控件If you're more interested in how the player controls update the game state, then see Adding controls.