Your First Game UI
Render HTML to an in-game texture using the CPU renderer.
On this page
This tutorial walks through rendering a web-page on the CPU, copying it to an engine texture, and forwarding native mouse and keyboard events.
๐ Using the CPU renderer
The CPU renderer is the easiest way to startโ for rendering directly on the GPU, see GPU Renderer Overview.
1. Set Up the Platform
Ultralight delegates most platform-specific tasks to the Platform singleton for stuff like loading files (eg, file:// URLs), copying to the clipboard, and logging.
To help you get started, the (optional) AppCore module provides ready-made helpers for these handlers. (You can use these without calling App::Create() to retain control over your run loop.)
#include <Ultralight/Ultralight.h>
#include <AppCore/Platform.h>
using namespace ultralight;
void InitPlatform() {
///
/// Use the OS's native font loader.
///
Platform::instance().set_font_loader(GetPlatformFontLoader());
///
/// Use the OS's native file system, with a base directory of "./assets/".
/// All file:/// URLs will load relative to this base directory.
///
Platform::instance().set_file_system(GetPlatformFileSystem("./assets/"));
///
/// Use the default logger (writes to a log file).
///
Platform::instance().set_logger(GetDefaultLogger("ultralight.log"));
}
#include <AppCore/CAPI.h>
void InitPlatform(void) {
///
/// Use the OS's native font loader.
///
ulEnablePlatformFontLoader();
///
/// Use the OS's native file system, with a base directory of "./assets/".
/// All file:/// URLs will load relative to this base directory.
///
ULString base_dir = ulCreateString("./assets/");
ulEnablePlatformFileSystem(base_dir);
ulDestroyString(base_dir);
///
/// Use the default logger (writes to a log file).
///
ULString log_path = ulCreateString("ultralight.log");
ulEnableDefaultLogger(log_path);
ulDestroyString(log_path);
}
๐ง Set handlers before creating the renderer
You should register platform handlers before calling
Renderer::Create(). If the library needs a font loader or file system and none is set, it exits the process with an error.
๐ Logging and custom handlers
The default logger writes to a fileโ to route messages to an engine log instead, register a custom
Logger(see Logging and Console Messages). To provide a custom font loader or file system, see Setting Up the Platform.
2. Create the Renderer and View
Initialize the Renderer singleton, then create an offscreen View to load page content.
RefPtr<Renderer> renderer;
RefPtr<View> view;
void CreateRendererAndView() {
///
/// Create our Renderer (call this only once per application, after
/// setting up the Platform).
///
renderer = Renderer::Create();
///
/// Create an offscreen View, 500 by 500 pixels large. Views use the
/// CPU renderer by default.
///
view = renderer->CreateView(500, 500, ViewConfig(), nullptr);
///
/// Load raw HTML into the View asynchronously.
///
view->LoadHTML("<h1>Hello from your game!</h1>");
}
static ULRenderer renderer = NULL;
static ULView view = NULL;
void CreateRendererAndView(void) {
///
/// Create our Renderer (call this only once per application, after
/// setting up the Platform). The Config is passed here in C, and is
/// destroyed once the Renderer exists.
///
ULConfig config = ulCreateConfig();
renderer = ulCreateRenderer(config);
ulDestroyConfig(config);
///
/// Create an offscreen View, 500 by 500 pixels large. Views use the
/// CPU renderer by default (NULL selects the default session).
///
ULViewConfig view_config = ulCreateViewConfig();
view = ulCreateView(renderer, 500, 500, view_config, NULL);
ulDestroyViewConfig(view_config);
///
/// Load raw HTML into the View asynchronously.
///
ULString html = ulCreateString("<h1>Hello from your game!</h1>");
ulViewLoadHTML(view, html);
ulDestroyString(html);
}
void Shutdown(void) {
///
/// Destroy the View, then the Renderer (anything we create, we destroy).
///
ulDestroyView(view);
ulDestroyRenderer(renderer);
}
Renderer::CreateView() allocates an offscreen drawing surface at the given pixel dimensions, rendering on the CPU by default.
Load Content and Shut Down
View::LoadHTML() loads raw HTML strings asynchronously into the page. To load local files or remote pages instead, call View::LoadURL() (file:/// requests resolve through the registered file system).
When shutting down, destroy all active View instances before releasing the Renderer.
3. Run the Frame Loop
Updating the renderer inside the game loop requires three calls split between logic updates and frame presentation.
void UpdateLogic() {
///
/// Give the library a chance to handle pending tasks and timers.
/// Call this as often as you can from your game loop.
///
renderer->Update();
}
void RenderOneFrame() {
///
/// Notify the renderer that the main display has refreshed.
///
renderer->RefreshDisplay(0);
///
/// Render all active Views.
///
renderer->Render();
}
void UpdateLogic(void) {
///
/// Give the library a chance to handle pending tasks and timers.
/// Call this as often as you can from your game loop.
///
ulUpdate(renderer);
}
void RenderOneFrame(void) {
///
/// Notify the renderer that the main display has refreshed.
///
ulRefreshDisplay(renderer, 0);
///
/// Render all active Views.
///
ulRender(renderer);
}
Update Logic
Renderer::Update() processes internal timers, network tasks, and JavaScript callbacks. Call this as frequently as possible from your main game loop.
Present Frames
Renderer::RefreshDisplay(0) advances CSS animations, smooth scrolling, and requestAnimationFrame() callbacks. Call this once per frame.
Renderer::Render() repaints any active views that have changed since the last frame into their pixel surfaces.
For details on display IDs, presentation timestamps, and repaint scheduling, see Updating and Rendering.
4. Copy Pixels to an Engine Texture
When using the CPU renderer, check whether the view was repainted and copy the updated pixels to an engine texture.
void CopyToTexture() {
Surface* surface = view->surface();
if (surface->dirty_bounds().IsEmpty())
return; // nothing changed since the last upload
// The default Surface is a BitmapSurface.
RefPtr<Bitmap> bitmap = static_cast<BitmapSurface*>(surface)->bitmap();
auto pixels = bitmap->LockPixelsSafe(); // unlocks at the end of scope
UploadToEngineTexture(pixels.data(), bitmap->width(), bitmap->height(),
bitmap->row_bytes());
surface->ClearDirtyBounds();
}
void CopyToTexture(void) {
///
/// Get the View's Surface and check if the pixels have changed.
///
ULSurface surface = ulViewGetSurface(view);
if (ulIntRectIsEmpty(ulSurfaceGetDirtyBounds(surface)))
return;
///
/// Get the underlying Bitmap (the default Surface is a BitmapSurface,
/// so cast to it). The Bitmap is owned by the Surface-- don't destroy it.
///
ULBitmap bitmap = ulBitmapSurfaceGetBitmap((ULBitmapSurface)surface);
///
/// Lock the Bitmap to access the raw pixels. The format is BGRA,
/// 8 bits per channel, premultiplied alpha.
///
void* pixels = ulBitmapLockPixels(bitmap);
///
/// Pseudo-code, upload the pixels to one of your engine's textures here.
///
UploadToEngineTexture(pixels, ulBitmapGetWidth(bitmap),
ulBitmapGetHeight(bitmap), ulBitmapGetRowBytes(bitmap));
///
/// Unlock the Bitmap and clear the dirty bounds when we're done.
///
ulBitmapUnlockPixels(bitmap);
ulSurfaceClearDirtyBounds(surface);
}
Pixel Format and Stride
Dirty Bounds
Surface::dirty_bounds() returns the bounding rectangle of pixels that changed since the last call to Surface::ClearDirtyBounds(). If this rectangle is empty, nothing painted and you can skip the texture upload.
Pixel Format
Pixels are stored in 32-bit BGRA format with premultiplied alpha. Use Bitmap::row_bytes() for the row stride during upload (rows may be padded).
Surface Allocation
The View owns the Surface, so you should not destroy itโ to render directly into memory you own, see Render Surfaces.
5. Forward Input
Forward input events from the engine to the View so the page can respond to user actions.
void OnEngineMouseMove(int x, int y) {
///
/// Create a MouseEvent and pass it to the View.
///
MouseEvent evt;
evt.type = MouseEvent::kType_MouseMoved;
evt.x = x;
evt.y = y;
evt.button = MouseEvent::kButton_None;
view->FireMouseEvent(evt);
}
MouseEvent specifies the event type (kType_MouseMoved, kType_MouseDown, or kType_MouseUp), the button pressed, and the cursor position. Pass the struct to View::FireMouseEvent() to dispatch it to the page.
๐ Convert to logical pixels
Coordinates in
MouseEventmust be in logical pixels relative to the top-left corner of theView. If the engine uses physical device pixels, divide the coordinates byView::device_scale()before firing the event.
Keyboard and Scroll Input
Keyboard and scroll events follow the same pattern through View::FireKeyEvent() and View::FireScrollEvent(). For details on constructing those events, see Mouse and Scroll Input and Keyboard Input.
6. Display the Texture on a Quad
Draw the texture to an on-screen quad to display it in your game (try to align texels with pixels for the sharpest results).
What's Next
Congrats! You should now have a live page of HTML rendering inside your game!
From here, Integrating into a Game Engine walks through GPU rendering and connecting UI events to game state.