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
APIas 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,UpdateSimplified game engine architecture similar to Unity/Unreal
Meant to be replaced by your engine’s actual functionality
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:
manages the API instance;
uses asynchronous data access using itself as the
eyeware::beam_eye_tracker::TrackingListener;shows how to launch the Beam Eye Tracker at
BeginPlay;computes HUD opacity values based on whether the user is looking at the HUD element or not, making said opacities part of the device output parameters;
samples the smoothed sim game camera state once per render frame, processes the camera pose parameters, maps them to the game engine coordinates, and sets the mapped
MyGameEngineTransform(yaw, pitch, roll, x, y, z) as a device output parameter;processes how game settings affect the device behavior and output.
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.
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
MyGameEngineImmersiveHUDconsisting of aMyGameEngineHUDElementinstance on each of the 4 corners of the viewport;Creates a scene in which a camera
MyGameEngineImmersiveCamerais positioned as a child of a character headMyGameEngineCharacterHead, which provides a reference pose (see Control the in-game camera movement);Both the
MyGameEngineImmersiveCameraandMyGameEngineCharacterHeadinstances hold pointers of theMyGameEngineBeamEyeTrackerDeviceinstance so that they react to eye and head tracking data at theTickfunction;Implements a
MyGameEngineHotkeysMapperwhich monitors if the user presses the SPACE key to recenter the camera, following Implement the camera recentering;
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.