Game Engine Integration

The Game Engine Integration Sample demonstrates how to integrate the Beam Eye Tracker SDK into a game engine architecture. To avoid dealing with the many complexities of a real game-engine, this sample uses a dummy game engine architecture. However, the coding patterns are compatible with major game engines like Unity and Unreal Engine 5. It demonstrates:

  • Wrapping the API as a custom game engine input device;

  • Implementation of In-game camera control;

  • Implementation of an Immersive HUD as in Dynamic HUDs;

  • Holding game settings parameters and mapping them to input device parameters;

  • Viewport geometry management;

  • Camera recentering functionality, and;

  • Auto-start capability for seamless user experience.

Note

Requirements: Windows 10 or newer, Linux, or macOS; CMake; a C++ compiler; and the Beam Eye Tracker application.

Language: C++.

Compiling and running the sample

As part of the C++ samples, you can find the game_engine_integration sample in the cpp/samples folder.

Prebuilt binaries may be included in cpp/samples/bin depending on the SDK package. You can also compile the sample yourself. To do so, run build_samples.bat on Windows or build_samples.sh on Linux/macOS from the cpp/samples folder. The script assumes you have CMake installed and that CMake can find a suitable C++ compiler/generator on your system. The CMakeLists.txt integrates the Beam Eye Tracker SDK as explained in Project configuration.

The program runs for 30 seconds. The console displays:

  • Real-time camera position and rotation (only z translation and yaw are shown for simplicity);

  • Current HUD opacity values which change whether you are looking at the top-left corner of the viewport, and;

  • Recentering status updates (when pressing SPACE).

Moreover, you can observe that the camera’s z translation continuously increases to simulate the reference camera pose changes through the 3D world scene, while the immersive camera pose changes are additive to this base pose.

Warning

The current viewport geometry is hardcoded in get_rendering_area_viewport_geometry_from_engine and thus you won’t experience realistic behavior out of the box when running the sample. It assumes the game is rendering on the middle screen of three 1920x1080 monitors, with the left-most screen being the Windows main display.

If you want realistic behavior, for example, that the opacity output changes when you indeed look at the top-left corner of YOUR display, you need to modify the get_rendering_area_viewport_geometry_from_engine function to return a rectangle geometry that matches your setup (e.g. a given screen or window that you want to pretend is game-rendering area), but do respect the Unity-like viewport definition (bottom-left corner at (0, 0)) which is assumed for this specific sample.

Sample explained

The source code is distributed into three files:

my_game_engine.h

This file implements dummy classes that emulate a typical game engine object-oriented pattern:

  • Base interfaces with common functions like BeginPlay, Tick, Update

  • Simplified game engine architecture similar to Unity/Unreal

  • Meant to be replaced by your engine’s actual functionality

my_game_engine.h
 1#ifndef MY_GAME_ENGINE_H
 2#define MY_GAME_ENGINE_H
 3
 4#include <vector>
 5/**
 6 * Copyright (C) 2025 Eyeware Tech SA.
 7 *
 8 * All rights reserved.
 9 *
10 * This file defines an extremely simplified game engine using OOP, similar to Unity and UE5
11 * OOP paradigm. In real life we assume all of this is already defined in your engine.
12 *
13 * The good stuff is in files main.cpp and bet_game_engine_device.cpp.
14 */
15
16struct MyGameEngineTransform {
17    // For this sample's purpose, we assume Unity's camera coordinate system which is the same as
18    // Beam, except that x is inverted, and the rotations are left-handed, not right-handed.
19    float rotation_x_degrees;
20    float rotation_y_degrees;
21    float rotation_z_degrees;
22    float translation_x_inches;
23    float translation_y_inches;
24    float translation_z_inches;
25
26    MyGameEngineTransform operator+(MyGameEngineTransform other) {
27        return MyGameEngineTransform{this->rotation_x_degrees + other.rotation_x_degrees,
28                                     this->rotation_y_degrees + other.rotation_y_degrees,
29                                     this->rotation_z_degrees + other.rotation_z_degrees,
30                                     this->translation_x_inches + other.translation_x_inches,
31                                     this->translation_y_inches + other.translation_y_inches,
32                                     this->translation_z_inches + other.translation_z_inches};
33    }
34};
35
36class MyGameEngineObjectInterface {
37  public:
38    MyGameEngineObjectInterface(MyGameEngineObjectInterface *parent) : parent(parent) {}
39    // Called frequently and periodically. delta_time in seconds.
40    virtual void Tick(float delta_time) {};
41
42    // Called when the rendering loop starts.
43    virtual void BeginPlay() {};
44
45    // Called when the rendering loop stops.
46    virtual void EndPlay() {};
47
48    void set_local_transform(MyGameEngineTransform local_transform) {
49        this->local_transform = local_transform;
50        if (this->parent) {
51            this->world_transform = this->parent->world_transform + this->local_transform;
52        } else {
53            this->world_transform = this->local_transform;
54        }
55    }
56
57    MyGameEngineTransform local_transform{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
58    MyGameEngineTransform world_transform{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
59
60    MyGameEngineObjectInterface *parent;
61};
62
63class MyGameEngineHUDElement : public MyGameEngineObjectInterface {
64  public:
65    enum class Type {
66        TOP_LEFT,
67        TOP_RIGHT,
68        BOTTOM_LEFT,
69        BOTTOM_RIGHT,
70    };
71
72    MyGameEngineHUDElement(MyGameEngineHUDElement::Type type, MyGameEngineObjectInterface *parent)
73        : MyGameEngineObjectInterface(parent), type(type) {}
74
75    Type type;
76    float opacity = 1.0f;
77};
78
79#endif // MY_GAME_ENGINE_H

bet_game_engine_device.h

Implements the MyGameEngineBeamEyeTrackerDevice which is the key class that you would typically implement for your specific game engine. You will see that it:

Note

The sample uses asynchronous data access, but if you can’t guarantee that the TrackerListener will be alive until the handle is explicitly deregistered, you can use polling data access instead, which also does not block the main thread.

bet_game_engine_device.h
  1/**
  2 * Copyright (C) 2025 Eyeware Tech SA.
  3 *
  4 * All rights reserved.
  5 */
  6
  7#ifndef BET_GAME_ENGINE_DEVICE_H
  8#define BET_GAME_ENGINE_DEVICE_H
  9
 10#include "eyeware/beam_eye_tracker.h"
 11#include "my_game_engine.h"
 12#include <algorithm>
 13#include <cstdlib>
 14#include <exception>
 15#include <iostream>
 16#include <memory>
 17
 18namespace {
 19
 20constexpr float BET_SAMPLE_PI = 3.14159265358979323846f;
 21constexpr float BET_RADIANS_TO_DEGREES = 180.0f / BET_SAMPLE_PI;
 22constexpr float M_METERS_TO_INCHES = 39.3700787;
 23
 24float update_hud_opacity(float prev_opacity, bool looking_at_hud, float delta_time) {
 25    // Implements asymmetric linear opacity update. You can use a nicer animation curve.
 26    const float opacity_rate_on_looking_at_hud = 10.0f;     // Fully visible in max 0.1 seconds
 27    const float opacity_rate_on_not_looking_at_hud = -1.0f; // Fully invisible in max 1 seconds
 28    const float min_opacity = 0.2f; // In case the HUD should not fully disappear
 29
 30    const float opacity_update_rate =
 31        looking_at_hud ? opacity_rate_on_looking_at_hud : opacity_rate_on_not_looking_at_hud;
 32    return std::min(std::max(prev_opacity + opacity_update_rate * delta_time, min_opacity), 1.0f);
 33}
 34
 35} // namespace
 36
 37// Note, for this sample, we keep explicit mentions to the namespace eyeware::beam_eye_tracker
 38// to make it easier to separate API related code.
 39class MyGameEngineBeamEyeTrackerDevice : public eyeware::beam_eye_tracker::TrackingListener,
 40                                         public MyGameEngineObjectInterface {
 41  public:
 42    MyGameEngineBeamEyeTrackerDevice(MyGameEngineObjectInterface *parent)
 43        : MyGameEngineObjectInterface(parent) {
 44        // We only need one instance of the API. You can also create it on "Begin Play" if you
 45        // want to, but here it is created in the constructor for simplicity and not to check
 46        // on pointer validity.
 47        try {
 48            m_bet_api = std::make_unique<eyeware::beam_eye_tracker::API>(
 49                "Game Engine Integration Sample",
 50                get_rendering_area_viewport_geometry_from_engine());
 51        } catch (const std::exception &error) {
 52            std::cerr << "Failed to create Beam Eye Tracker API: " << error.what() << std::endl;
 53            std::exit(EXIT_FAILURE);
 54        }
 55    }
 56
 57    ~MyGameEngineBeamEyeTrackerDevice() { stop_bet_api_tracking_data_reception(); }
 58
 59    void BeginPlay() override {
 60        if (m_auto_start_tracking) {
 61            // If auto start is toggled on, this will request the Beam app to launch and
 62            // or to start the webcam and initialize the tracking.
 63            // HEADS UP! Be wise when you call this. Ideally you want to call it when the game
 64            // rendering starts and accepts device input, as otherwise may start the webcam
 65            // at a random time and confuse the user.
 66            m_bet_api->attempt_starting_the_beam_eye_tracker();
 67        }
 68
 69        // Register itself as the listener to receive tracking data from the Beam Eye Tracker
 70        // application on the on_tracking_state_set_update method asynchronously.
 71        if (m_listener_handle == eyeware::beam_eye_tracker::INVALID_TRACKING_LISTENER_HANDLE) {
 72            m_listener_handle = m_bet_api->start_receiving_tracking_data_on_listener(this);
 73        }
 74    }
 75
 76    void EndPlay() override { stop_bet_api_tracking_data_reception(); }
 77
 78    void Tick(float delta_time) override {
 79        // For the purpose of this sample, we assume a custom device output is the HUD opacity,
 80        // which is updated here.
 81
 82        // Animate the opacity change depending on whether the user is looking at HUD elements.
 83        device_output_top_left_hud_opacity = update_hud_opacity(
 84            device_output_top_left_hud_opacity, m_is_user_looking_at_top_left_corner, delta_time);
 85        device_output_top_right_hud_opacity = update_hud_opacity(
 86            device_output_top_right_hud_opacity, m_is_user_looking_at_top_right_corner, delta_time);
 87        device_output_bottom_left_hud_opacity =
 88            update_hud_opacity(device_output_bottom_left_hud_opacity,
 89                               m_is_user_looking_at_bottom_left_corner, delta_time);
 90        device_output_bottom_right_hud_opacity =
 91            update_hud_opacity(device_output_bottom_right_hud_opacity,
 92                               m_is_user_looking_at_bottom_right_corner, delta_time);
 93
 94        // Update viewport every 3 seconds in case the rendering area geometry changed.
 95        // In the Beam Eye Tracker application API, this operation is light-weight
 96        // so you could call it more frequently, but 3 seconds balances the trade-off between
 97        // slight-overhead (inc. game engine retrieval of the geometry) and responsiveness in case
 98        // the game was resized or moved. If you have an specific event for game window geometry
 99        // changes, may be better suited for this purpose.
100        m_time_since_last_viewport_geometry_update += delta_time;
101        if (m_time_since_last_viewport_geometry_update >= 3.0f) {
102            m_bet_api->update_viewport_geometry(get_rendering_area_viewport_geometry_from_engine());
103            m_time_since_last_viewport_geometry_update = 0.0f;
104        }
105    }
106
107    // Functions for recentering the camera. Likely mapped to a hotkey press/release event.
108    void recenter_camera_start() {
109        if (m_bet_api) {
110            m_bet_api->recenter_sim_game_camera_start();
111        }
112    }
113    void recenter_camera_end() {
114        if (m_bet_api) {
115            m_bet_api->recenter_sim_game_camera_end();
116        }
117    }
118
119    eyeware::beam_eye_tracker::ViewportGeometry get_rendering_area_viewport_geometry_from_engine() {
120        // Implement here your Game Engine specific logic where you retrieve the rendering area
121        // geometry, i.e., the viewport. We need to keep the eyeware::beam_eye_tracker::API up to
122        // date with changes in the viewport geometry.
123
124        // For this demo, assume this configuration: three physical monitors from left to right of
125        // resolutions 1920x1080, 1920x1080, 1920x1080. The left-most monitor is configured in
126        // Windows settings as the "Main display" (thus, it defines the (0, 0) coordinates in the
127        // Windows Virtual Screen), and the game is rendering full screen in the center monitor.
128        // Moreover, lets assume this game engine follows Unity's viewport standard, where the the
129        // viewport (0, 0) coordinates are at the bottom-left corner of the rendering area.
130        // Thus this coordinates would represent that configuration:
131        eyeware::beam_eye_tracker::Point point_00 = {1920, 1079};
132        eyeware::beam_eye_tracker::Point point_11 = {1920 + 1920 - 1, 0};
133
134        return {point_00, point_11};
135    }
136
137    void on_tracking_data_reception_status_changed(
138        eyeware::beam_eye_tracker::TrackingDataReceptionStatus status) override {
139
140        // See TrackingDataReceptionStatus for explanation on all possible statuses.
141        if (status ==
142            eyeware::beam_eye_tracker::TrackingDataReceptionStatus::NOT_RECEIVING_TRACKING_DATA) {
143            // If the tracking data reception status is NOT_RECEIVING_TRACKING_DATA is because the
144            // Beam app is not open, the webcam is not active, the client was rejected from using
145            // the API, the user is not signed in, etc. etc.. To "fix" this, manual intervention
146            // from the user is required. Note that this state could be reached shortly after a call
147            // to attempt_starting_the_beam_eye_tracker, which failed to achieve the auto-start.
148            // You can try calling attempt_starting_the_beam_eye_tracker again, but it is a question
149            // of user experience, as the user may be manually toggling off, but the game insists on
150            // toggling on.
151
152            // In this situation, makes sense to reset the device output to default values.
153            // Animation curves could be used for a smoother transition.
154            device_output_camera_local_transform = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
155
156            device_output_top_left_hud_opacity = 1.0f;
157            device_output_top_right_hud_opacity = 1.0f;
158            device_output_bottom_left_hud_opacity = 1.0f;
159            device_output_bottom_right_hud_opacity = 1.0f;
160
161            m_is_user_looking_at_top_left_corner = true;
162            m_is_user_looking_at_top_right_corner = true;
163            m_is_user_looking_at_bottom_left_corner = true;
164            m_is_user_looking_at_bottom_right_corner = true;
165        }
166    }
167
168    void on_tracking_state_set_update(
169        const eyeware::beam_eye_tracker::TrackingStateSet &tracking_state_set,
170        const eyeware::beam_eye_tracker::Timestamp timestamp) override {
171        // Async callback to retrieve the tracking data.
172        //
173        // Note: the SimGameCameraState held by tracking_state_set is a snapshot at the tracking
174        // rate and is intentionally NOT consumed here. Instead, the immersive camera samples
175        // the smoothed signal once per render frame via
176        // sample_smoothed_sim_game_camera_state_for_current_render_frame() below. This delivers
177        // a continuous in-game camera at the render rate rather than at the tracking rate.
178
179        update_device_viewport_gaze_state_from_bet_api_input(tracking_state_set.user_state());
180
181        update_device_game_immersive_hud_state_from_bet_api_input(
182            tracking_state_set.game_immersive_hud_state());
183    }
184
185    /**
186     * @brief Sample the smoothed sim camera state for the current render frame.
187     *
188     * Called once per Tick from the immersive camera object. Maps the smoothed pose to the
189     * game engine's camera coordinate system and writes the result into
190     * device_output_camera_local_transform.
191     */
192    void sample_smoothed_sim_game_camera_state_for_current_render_frame() {
193        // Mapping sensitivity (default 0.5) to weight (default 1.0). Note this mapping
194        // could be more complex, but the assumption is that a weight of 1.0 would make the
195        // signal as configured by the user within the Beam Eye Tracker application.
196        const float sim_game_camera_eye_tracking_weight =
197            2.0f * m_sim_game_camera_eye_tracking_sensitivity;
198        const float sim_game_camera_head_tracking_weight =
199            2.0f * m_sim_game_camera_head_tracking_sensitivity;
200
201        const eyeware::beam_eye_tracker::SimGameCameraState smoothed_state =
202            m_bet_api->sample_smooth_sim_game_camera_state();
203        if (smoothed_state.timestamp_in_seconds == eyeware::beam_eye_tracker::NULL_DATA_TIMESTAMP) {
204            // No tracking input received yet, or filter params not yet synchronized. Keep the
205            // previous transform: snapping to zero on a momentary interruption (e.g. user
206            // briefly off-frame) would be a confusing user experience.
207            return;
208        }
209
210        // This combines the signals into one transform.
211        eyeware::beam_eye_tracker::SimCameraTransform3D bet_camera_local_transform =
212            m_bet_api->compute_smooth_sim_game_camera_transform_parameters(
213                smoothed_state, sim_game_camera_eye_tracking_weight,
214                sim_game_camera_head_tracking_weight);
215
216        // Map Beam coordinates to the game engine's coordinate system. Assuming the game
217        // engine is using Unity's coordinate system (same as Beam, except that x is inverted
218        // and the rotations are left-handed instead of right-handed).
219        device_output_camera_local_transform.rotation_x_degrees =
220            bet_camera_local_transform.pitch_in_radians * BET_RADIANS_TO_DEGREES;
221        device_output_camera_local_transform.rotation_y_degrees =
222            -bet_camera_local_transform.yaw_in_radians * BET_RADIANS_TO_DEGREES;
223        device_output_camera_local_transform.rotation_z_degrees =
224            -bet_camera_local_transform.roll_in_radians * BET_RADIANS_TO_DEGREES;
225
226        device_output_camera_local_transform.translation_x_inches =
227            -bet_camera_local_transform.x_in_meters * M_METERS_TO_INCHES;
228        device_output_camera_local_transform.translation_y_inches =
229            bet_camera_local_transform.y_in_meters * M_METERS_TO_INCHES;
230        device_output_camera_local_transform.translation_z_inches =
231            bet_camera_local_transform.z_in_meters * M_METERS_TO_INCHES;
232    }
233
234    void update_device_viewport_gaze_state_from_bet_api_input(
235        const eyeware::beam_eye_tracker::UserState &user_state) {
236
237        // Eye tracking coordinates referred to the viewport area.
238        if (user_state.timestamp_in_seconds != eyeware::beam_eye_tracker::NULL_DATA_TIMESTAMP &&
239            eyeware::beam_eye_tracker::cast_confidence(user_state.viewport_gaze.confidence) !=
240                eyeware::beam_eye_tracker::TrackingConfidence::LOST_TRACKING) {
241
242            // Normalized gaze coordinates in the viewport. Normalized as it is in the range [0, 1],
243            // however, values outside this range are possible.
244            device_output_viewport_normalized_gaze_x =
245                user_state.viewport_gaze.normalized_point_of_regard.x;
246            device_output_viewport_normalized_gaze_y =
247                user_state.viewport_gaze.normalized_point_of_regard.y;
248        }
249    }
250
251    void update_device_game_immersive_hud_state_from_bet_api_input(
252        const eyeware::beam_eye_tracker::GameImmersiveHUDState &game_immersive_hud_state) {
253        if (game_immersive_hud_state.timestamp_in_seconds !=
254            eyeware::beam_eye_tracker::NULL_DATA_TIMESTAMP) {
255
256            // Note: the input values are interpreted a "likelihood" or as a "probability", so
257            // you can simply threshold it.
258            m_is_user_looking_at_top_left_corner =
259                game_immersive_hud_state.looking_at_viewport_top_left > 0.5f;
260            m_is_user_looking_at_top_right_corner =
261                game_immersive_hud_state.looking_at_viewport_top_right > 0.5f;
262            m_is_user_looking_at_bottom_left_corner =
263                game_immersive_hud_state.looking_at_viewport_bottom_left > 0.5f;
264            m_is_user_looking_at_bottom_right_corner =
265                game_immersive_hud_state.looking_at_viewport_bottom_right > 0.5f;
266
267        } else {
268            // There could be multiple reasons to receive a NULL_DATA_TIMESTAMP in the callback.
269            // But in general it means an interruption of the normal tracking, the feature
270            // itself, or other.
271
272            // In this case, it makes sense to "reset" at set all the HUD as visible. For
273            // example, assume the user is off-camera.
274            m_is_user_looking_at_top_left_corner = true;
275            m_is_user_looking_at_top_right_corner = true;
276            m_is_user_looking_at_bottom_left_corner = true;
277            m_is_user_looking_at_bottom_right_corner = true;
278        }
279    }
280
281    //*********  VARIABLES ASSUMED TO BE LINKED TO IN-GAME SETTINGS ****************** */
282    // Note: we don't define the setters to avoid clutter.
283    /**
284     * @brief Implement a user interface that allows to change this value.
285     */
286    bool m_auto_start_tracking = true;
287
288    // For the in-game camera controls, assumed to be in the range [0, 1].
289    float m_sim_game_camera_eye_tracking_sensitivity = 0.5f;
290    float m_sim_game_camera_head_tracking_sensitivity = 0.5f;
291
292    //************ VARIABLES REPRESENTING THE DEVICE STATE/OUTPUT ************ */
293    // Note: we don't define the getters to avoid clutter.
294
295    // Sim game camera local/additive transform
296    MyGameEngineTransform device_output_camera_local_transform{0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
297
298    // Immersive HUD state. Here it is default opaque,  i.e., all the HUD is visible until
299    // tracking data says otherwise. It's different than the boolean counterparts, as the
300    // opacity changes are smooth across frames.
301    float device_output_top_left_hud_opacity = 1.0f;
302    float device_output_top_right_hud_opacity = 1.0f;
303    float device_output_bottom_left_hud_opacity = 1.0f;
304    float device_output_bottom_right_hud_opacity = 1.0f;
305
306    // Normalized gaze coordinates in the viewport.
307    float device_output_viewport_normalized_gaze_x = 0.0f;
308    float device_output_viewport_normalized_gaze_y = 0.0f;
309
310    //************************************************************************************ */
311
312  private:
313    bool m_is_user_looking_at_top_left_corner = true;
314    bool m_is_user_looking_at_top_right_corner = true;
315    bool m_is_user_looking_at_bottom_left_corner = true;
316    bool m_is_user_looking_at_bottom_right_corner = true;
317
318    void stop_bet_api_tracking_data_reception() {
319        if (m_listener_handle != eyeware::beam_eye_tracker::INVALID_TRACKING_LISTENER_HANDLE) {
320            m_bet_api->stop_receiving_tracking_data_on_listener(m_listener_handle);
321            m_listener_handle = eyeware::beam_eye_tracker::INVALID_TRACKING_LISTENER_HANDLE;
322        }
323    }
324
325    std::unique_ptr<eyeware::beam_eye_tracker::API> m_bet_api;
326    eyeware::beam_eye_tracker::TRACKING_LISTENER_HANDLE m_listener_handle{
327        eyeware::beam_eye_tracker::INVALID_TRACKING_LISTENER_HANDLE};
328    float m_time_since_last_viewport_geometry_update{0.0f};
329};
330
331#endif // BET_GAME_ENGINE_DEVICE_H

main.cpp

This is the main function that manages the scene objects, the MyGameEngineBeamEyeTrackerDevice instance, and the game loop, in particular, it:

  • Creates a HUD MyGameEngineImmersiveHUD consisting of a MyGameEngineHUDElement instance on each of the 4 corners of the viewport;

  • Creates a scene in which a camera MyGameEngineImmersiveCamera is positioned as a child of a character head MyGameEngineCharacterHead, which provides a reference pose (see Control the in-game camera movement);

  • Both the MyGameEngineImmersiveCamera and MyGameEngineCharacterHead instances hold pointers of the MyGameEngineBeamEyeTrackerDevice instance so that they react to eye and head tracking data at the Tick function;

  • Implements a MyGameEngineHotkeysMapper which monitors if the user presses the SPACE key to recenter the camera, following Implement the camera recentering;

main.cpp
  1#include "bet_game_engine_device.h"
  2#include "my_game_engine.h"
  3#include <chrono>
  4#include <iomanip>
  5#include <iostream>
  6#include <memory>
  7#include <thread>
  8
  9#if defined(_WIN32)
 10#include <windows.h>
 11#elif defined(__APPLE__)
 12#include <ApplicationServices/ApplicationServices.h>
 13#include <Carbon/Carbon.h>
 14#elif defined(__linux__)
 15#include <atomic>
 16#include <limits>
 17#else
 18#error                                                                                             \
 19    "game_engine_integration sample: unsupported platform (Windows, macOS, and Linux are supported)"
 20#endif
 21
 22namespace {
 23std::vector<MyGameEngineObjectInterface *> devices;
 24
 25#if defined(__linux__)
 26// Toggled on each Enter in LinuxRecenterInputThread: first press simulates key-down, second
 27// simulates key-up, matching start/end edges in Tick like Windows/macOS hold detection.
 28std::atomic<bool> g_linux_recenter_virtually_held{false};
 29
 30void LinuxRecenterInputThread() {
 31    while (true) {
 32        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
 33        const bool cur = g_linux_recenter_virtually_held.load();
 34        g_linux_recenter_virtually_held.store(!cur);
 35    }
 36}
 37#endif
 38
 39bool IsRecenterKeyDown() {
 40#if defined(_WIN32)
 41    return (GetAsyncKeyState(VK_SPACE) & 0x8000) != 0;
 42#elif defined(__APPLE__)
 43    return CGEventSourceKeyState(kCGEventSourceStateCombinedSessionState, kVK_Space) != 0;
 44#elif defined(__linux__)
 45    return g_linux_recenter_virtually_held.load();
 46#endif
 47}
 48} // namespace
 49
 50// See bet_game_engine_device.h for showing the "integration" of the Beam API as a device in the
 51// engine See this file to see how it interacts with the other objects in the engine.
 52class MyGameEngineImmersiveHUD : public MyGameEngineObjectInterface {
 53  public:
 54    MyGameEngineImmersiveHUD(MyGameEngineObjectInterface *parent)
 55        : MyGameEngineObjectInterface(parent) {}
 56
 57    void BeginPlay() override {
 58        // Get a reference to the Beam Eye Tracker device.
 59        beam_eye_tracker_device = dynamic_cast<MyGameEngineBeamEyeTrackerDevice *>(devices.at(0));
 60
 61        // Dummy hud ui_elements added to all corners
 62        ui_elements.push_back(MyGameEngineHUDElement{MyGameEngineHUDElement::Type::TOP_LEFT, this});
 63        ui_elements.push_back(
 64            MyGameEngineHUDElement{MyGameEngineHUDElement::Type::TOP_RIGHT, this});
 65        ui_elements.push_back(
 66            MyGameEngineHUDElement{MyGameEngineHUDElement::Type::BOTTOM_LEFT, this});
 67        ui_elements.push_back(
 68            MyGameEngineHUDElement{MyGameEngineHUDElement::Type::BOTTOM_RIGHT, this});
 69    }
 70    // Implemented in bet_game_engine_device.cpp to identify
 71    void Tick(float delta_time) override {
 72        for (auto &element : ui_elements) {
 73            switch (element.type) {
 74            case MyGameEngineHUDElement::Type::TOP_LEFT:
 75                element.opacity = beam_eye_tracker_device->device_output_top_left_hud_opacity;
 76                break;
 77            case MyGameEngineHUDElement::Type::TOP_RIGHT:
 78                element.opacity = beam_eye_tracker_device->device_output_top_right_hud_opacity;
 79                break;
 80            case MyGameEngineHUDElement::Type::BOTTOM_LEFT:
 81                element.opacity = beam_eye_tracker_device->device_output_bottom_left_hud_opacity;
 82                break;
 83            case MyGameEngineHUDElement::Type::BOTTOM_RIGHT:
 84                element.opacity = beam_eye_tracker_device->device_output_bottom_right_hud_opacity;
 85                break;
 86            }
 87        }
 88    }
 89
 90    std::vector<MyGameEngineHUDElement> ui_elements;
 91
 92    MyGameEngineBeamEyeTrackerDevice *beam_eye_tracker_device{nullptr};
 93};
 94
 95class MyGameEngineImmersiveCamera : public MyGameEngineObjectInterface {
 96  public:
 97    MyGameEngineImmersiveCamera(MyGameEngineObjectInterface *parent)
 98        : MyGameEngineObjectInterface(parent) {}
 99
100    void BeginPlay() override {
101        // Get a reference to the Beam Eye Tracker device.
102        beam_eye_tracker_device = dynamic_cast<MyGameEngineBeamEyeTrackerDevice *>(devices.at(0));
103    }
104
105    void Tick(float delta_time) override {
106        // Sample the smoothed sim game camera state from the Beam API for the current render
107        // frame. This is the recommended pattern for an in-game camera: smoothing rate adapts
108        // to the render loop rather than to the tracking rate.
109        beam_eye_tracker_device->sample_smoothed_sim_game_camera_state_for_current_render_frame();
110
111        // Updates the local pose.
112        // What is critical to notice is that this updates the world_transform by adding up the
113        // parent's world_transform with the now given local_transform.
114        this->set_local_transform(beam_eye_tracker_device->device_output_camera_local_transform);
115    }
116
117    MyGameEngineBeamEyeTrackerDevice *beam_eye_tracker_device{nullptr};
118};
119
120class MyGameEngineHotkeysMapper : public MyGameEngineObjectInterface {
121  public:
122    MyGameEngineHotkeysMapper(MyGameEngineObjectInterface *parent)
123        : MyGameEngineObjectInterface(parent) {
124#if defined(__linux__)
125        std::thread(LinuxRecenterInputThread).detach();
126#endif
127    }
128
129    void BeginPlay() override {
130        // Get a reference to the Beam Eye Tracker device.
131        beam_eye_tracker_device = dynamic_cast<MyGameEngineBeamEyeTrackerDevice *>(devices.at(0));
132    }
133    void Tick(float delta_time) override {
134        const bool recenter_key_pressed = IsRecenterKeyDown();
135        if (recenter_key_pressed != was_recenter_key_pressed) {
136            if (recenter_key_pressed) {
137                beam_eye_tracker_device->recenter_camera_start();
138            } else {
139                beam_eye_tracker_device->recenter_camera_end();
140            }
141            was_recenter_key_pressed = recenter_key_pressed;
142        }
143    }
144
145    bool was_recenter_key_pressed = false;
146    MyGameEngineBeamEyeTrackerDevice *beam_eye_tracker_device{nullptr};
147};
148
149class MyGameEngineCharacterHead : public MyGameEngineObjectInterface {
150  public:
151    MyGameEngineCharacterHead(MyGameEngineObjectInterface *parent)
152        : MyGameEngineObjectInterface(parent) {}
153
154    void Tick(float delta_time) override {
155        // Just pretend the character is moving forward very slowly.
156        this->world_transform.translation_z_inches += 0.01f * M_METERS_TO_INCHES * delta_time;
157    }
158};
159
160int main() {
161    std::unique_ptr<MyGameEngineBeamEyeTrackerDevice> beam_eye_tracker_device =
162        std::make_unique<MyGameEngineBeamEyeTrackerDevice>(nullptr);
163    devices.push_back(beam_eye_tracker_device.get());
164
165    // Basic components, whose parent will be ignored as that's irrelevant in this sample.
166    MyGameEngineCharacterHead character_head(nullptr);
167    MyGameEngineImmersiveHUD immersive_hud(nullptr);
168    MyGameEngineHotkeysMapper hotkeys_mapper(nullptr);
169    MyGameEngineImmersiveCamera immersive_camera(&character_head);
170
171    // We could put all in a list, but will be made explicit for clarity.
172
173    //============= INITIALIZING THE GAME RENDERING =============
174    for (auto &device : devices) {
175        // Initializes the Beam device and API
176        device->BeginPlay();
177    }
178    hotkeys_mapper.BeginPlay();
179    immersive_hud.BeginPlay();
180    immersive_camera.BeginPlay();
181    character_head.BeginPlay();
182
183    //============= FRAME LOOP at 60 FPS =============
184    auto prev_frame_time = std::chrono::system_clock::now();
185    const auto end_time = prev_frame_time + std::chrono::seconds(30); // Run for 30 seconds.
186    while (std::chrono::system_clock::now() < end_time) {
187        auto frame_start = std::chrono::system_clock::now();
188        const float delta_time =
189            std::chrono::duration<float>(frame_start - prev_frame_time).count();
190        prev_frame_time = frame_start;
191
192        hotkeys_mapper.Tick(delta_time);
193        // We assume that devices are updated before the HUD and camera.
194        for (auto &device : devices) {
195            device->Tick(delta_time);
196        }
197        // In theory, here the parent-child relationship would drive the ordering, but we'll just
198        // fake it by updating the character head first, then the camera, then the HUD.
199        character_head.Tick(delta_time);
200        immersive_hud.Tick(delta_time);
201
202        immersive_camera.Tick(delta_time);
203
204        // Note: if you want to see "real" responses for the top-left HUD element opacity when you
205        // look to the top-left corner of your display, please edit
206        // MyGameEngineBeamEyeTrackerDevice::get_rendering_area_viewport_geometry_from_engine and
207        // hard-code the correct geometry of your display.
208
209        // Note: you should see the printed z values to grow slowly as the character head is moving
210        // forward slowly, but also increase or decrease in values as you move towards or away from
211        // the webcam. Hold SPACE to recenter (Windows/macOS); on Linux use Enter twice to
212        // simulate hold then release.
213        std::cout << "[Game cam: z_pos_inches=" << std::fixed << std::setprecision(2)
214                  << immersive_camera.world_transform.translation_z_inches
215                  << " ; yaw_degrees=" << std::fixed << std::setprecision(2)
216                  << immersive_camera.world_transform.rotation_y_degrees << "] and ["
217                  << "HUD top left opacity=" << std::fixed << std::setprecision(2)
218                  << immersive_hud.ui_elements.at(0).opacity << "]";
219        if (hotkeys_mapper.was_recenter_key_pressed) {
220            std::cout << " Recentering!" << std::endl;
221        } else {
222            std::cout << std::endl;
223        }
224
225        // Sleep for 16ms to simulate 60-ish FPS
226        std::this_thread::sleep_for(std::chrono::milliseconds(16));
227    }
228    //============= SHUTTING DOWN THE GAME RENDERING =============
229    for (auto &device : devices) {
230        device->EndPlay(); // Shuts down the Beam device and API
231    }
232    immersive_hud.EndPlay();
233    immersive_camera.EndPlay();
234    character_head.EndPlay();
235}

Other highlights

  • The sample assumes that the game engine is using a Unity-like coordinate system for the camera pose (yaw, pitch, roll, x, y, z definitions) and viewport geometry (origin (0, 0) at bottom-left corner of the rendering area). The Beam SDK camera state uses the SDK 2.2 rotation convention described in Mapping to the game engine coordinate system.

  • The demonstrated game settings are head tracking sensitivity, eye tracking sensitivity and auto-start behavior toggle, which would typically be exposed in the game’s UI.

  • Note how the opacity fade in and fade out is animated to make a smoother experience, but fade in is much faster than fade out.