Native VR port · Meta Quest 3

Unreal Tournament,
Native on Meta Quest.

It runs on the headset. Nothing is streamed from a PC, and it isn't a flat screen floating in front of you. I rebuilt the 1999 engine as a 64-bit ARM binary so it renders real stereo through OpenXR, with the gun in your hand and the whole original game behind it.

Download How it works Support my VR mods on Patreon

Mod by GHWST  ·  @GhwstVR

The Liandri tower and city skyline from the Unreal Tournament intro, rendered on a Quest 3 under a purple night sky.

Pulled straight off the headset. Left eye buffer, 1680×1760 per eye.

What it is

The whole game, in VR, standalone

Every map, every weapon, every bot, the menus, the mod support. This is actual Unreal Tournament, not something built to look like it. What I changed is everything sitting between the game and your eyes.

Real stereo

Two eyes, two separate frustums, handed to the compositor as a real projection layer. You can judge distance with it.

The gun is in your hand

The weapon is attached to your controller and fires where you point it. Shots come out of the barrel, not out of the middle of the model.

Lean, duck, peek

Moving your head moves the view in the world, so you can lean around a corner without moving your feet.

Menus in the room

The menus sit on a panel a couple of metres out, anchored where you opened them, and you point at them with a controller. The HUD stays level with the horizon no matter how you tilt your head.

Sniper scope that zooms

Alt-fire actually magnifies the view. It also switches your aim to your head and puts the crosshair dead centre, because aiming a scope with a controller at 8x is not workable.

Online works

The client runs the 469 packages that live servers use, and the app installs them for you, so you match what everyone else is running. Open the server browser, pick a game, play. This is new and I've tested it myself rather than extensively, so tell me if a server gives you trouble.

Screenshots

Straight off the device

No mock-ups, no PC renders. These came off a Quest 3 over adb. All I did was crop them.

Get it

You bring the game, I bring the VR

This is only the engine. It ships zero Unreal Tournament content, so no maps, no textures, no sounds, no music. You need your own copy of the game and you hand it to the app yourself.

Download APK v1.1.0 · arm64-v8a · Quest 3 · sideload

What's new in 1.1.0

Installs straight over 1.0.1 or 1.0.0. You don't need to uninstall and you don't need to import your game files again. This is the biggest update so far.

New, and I've watched each of these working on my own headset:

  • Haptics, for the whole game. Every weapon feels different, primary and alt fire separately. Explosions shake both controllers harder the bigger and closer they are. Damage, pickups, jumps, landings and menus all register. There's a toggle if you'd rather not
  • Every weapon's size, proportions and position in the hand tuned by hand, in the headset, one gun at a time. Mirrored properly when you swap hands or run dual enforcers
  • Alt fire is now the grip on your gun hand. Squeeze and it fires, no more holding grip and pulling trigger
  • The Redeemer's rocket steers with your head. Look where you want it to go. The stick works on top if you want it
  • Dual enforcers fire from either trigger
  • A VR Options menu under Mods: smooth turning, snap angle, turn speed, haptics, and a reset controls button
  • The announcer. He was never silent, he was standing in one spot on the map and you could only hear him from there. Now he's in your head where he belongs, at the right volume
  • The freeze during the intro, or when pressing menu as a level starts. This was the big one from the bug reports
  • The whole game was playing every sound about 9 percent too fast, since 1.0.0. It doesn't now
  • The triggers fire earlier in the pull, so shots feel immediate
  • X and Y cycle weapons, and the grips can be rebound in Preferences like any other button

Fixed in the code, and these are the ones I can't fully reproduce here because they need a Quest 2 or a broken first install. Tell me if you still hit one:

  • Quest 2: picking the data folder doing nothing. Android won't share the Download folder through its picker at all, so the app now finds your files in Download by itself and asks for permission once
  • The boot loop. Two separate causes closed: a broken settings file can no longer re-import itself, and losing storage permission now shows you the permission screen instead of dying silently
  • A faint whole scene shimmer in stereo. Each eye was rolling its own random brightness for every flickering light. They agree now
  • A rare crash from custom sounds at unusual sample rates

Also, I've set up a Patreon. If you want more VR mods like this one, that's the way to support them.

HeadsetMeta Quest 3. Developer mode turned on so you can sideload.
You need to ownUnreal Tournament (1999). GOTY or any full install. Epic pulled it from Steam and GOG at the end of 2022, so there's no storefront to point you at. If you bought it back then it's still in your library. You're on your own for this part, and I can't help with it.
SpaceA full install runs about 2 GB. The importer copies your files rather than moving them, so leave room for two copies while it runs, then delete the folder you imported from. More again once you start collecting maps online.
Wi-Fi, onceOn first run the app downloads the OldUnreal 469 patch straight from their GitHub releases, about 106 MB, and installs it over your copy. That's what makes every install identical no matter which version of UT99 you own. It happens once and you'll see a progress bar. If the headset is offline it will tell you and let you retry.
ControllersTouch controllers. The gun follows whichever hand you set as your gun hand.
  1. Sideload the APK

    SideQuest, or adb install -r ut99quest.apk with the headset plugged in.

  2. Give it your Unreal Tournament files

    Two ways to do this. Both end up in the same place.

    Easiest: copy your UT99 folder onto the headset, anywhere you like, as a folder or as a zip. Then launch the app. It'll tell you it can't find any game data and give you buttons for Select UT99 ZIP and Select UT99 folder. Point it at what you copied, tap Use this folder and then Allow, and it copies everything into place and launches straight into the game.

    Faster if you're comfortable with adb: push the folders straight into the app's own directory and skip the picker entirely. This is where the app looks by default, and it needs no permissions and no setup.

    adb push System   /sdcard/Android/data/com.ghwstvr.ut99quest/files/UT99/
    adb push Maps     /sdcard/Android/data/com.ghwstvr.ut99quest/files/UT99/
    adb push Textures /sdcard/Android/data/com.ghwstvr.ut99quest/files/UT99/
    adb push Sounds   /sdcard/Android/data/com.ghwstvr.ut99quest/files/UT99/
    adb push Music    /sdcard/Android/data/com.ghwstvr.ut99quest/files/UT99/
  3. Let it patch itself

    First run only. It fetches the OldUnreal 469 packages and lays them over your copy, with a progress bar. Takes a couple of minutes on a decent connection. Everyone ends up on the same version this way, which is the only reason any of this is predictable.

  4. Launch it

    It comes up in stereo. Everything after that is the normal Unreal Tournament menus and you point at them with a controller. Player Setup, the keyboard for typing your name, all of it works with the controllers.

What it's looking for. A normal UT99 install: System, Maps, Textures, Sounds and Music, with Core.u, Engine.u and Botpack.u inside System. If you point it one folder too high or too low it'll say so and let you try again.

The game has to run from the app's own folder. Anywhere else needs storage permission, which a sideload doesn't always grant, and when it isn't granted the app can't read the files and dies at startup. Staging your copy somewhere like /sdcard/UT99Quest is fine. The importer reads it through the system file picker, which needs no permission. Just let the importer move it in rather than trying to play from there. The adb path above puts it in the right place directly.

Worth saying plainly. This is a hobby port of a 25 year old game that you sideload onto a headset. It's not a store release, there's no support line, and it will do odd things. Back up anything you care about before you point tools at it.

How it works

What actually had to change

Unreal Engine 1 was written in 1998 for a 32-bit x86 PC with one screen. Hardly any of the real work here was VR features. Most of it was getting a rendering model that predates programmable shaders to produce something a modern compositor will accept.

64-bit, on ARM

The engine assumes 32-bit pointers in places that matter, like struct layouts that got saved to disk as a file format. On 64-bit those go wrong quietly instead of failing to compile.

Stereo without a view matrix

UE1 has no view matrix, so there isn't one to give two of. The separation comes purely from where each eye sits. Anything that moves an eye has to move the head and add the eye offset back, or the 3D quietly turns flat.

The 2D layer is a quad

The HUD, console and menus are flat 2D draws. They get mapped onto a plane sitting in the world so they have real depth, and the controller pointer runs that same mapping backwards to work out what you clicked.

Optical zoom, done properly

The scope divides the eye tangents rather than the angles, because that's what magnification actually is. The field of view handed to the compositor is deliberately left alone. Narrowing both cancels the zoom out completely, which I found out the hard way.

Downloads that finish

Joining a modded server meant pulling tens of megabytes through UT99's 1999 file transfer. It now downloads over HTTP and unpacks .uz files on the headset. Last measurement was 45 packages, 24.8 MB down and 50.5 MB on disk, in under a minute.

Measured, not assumed

Frame timing is logged per eye. Standing still it sits at the display's full 90 Hz with about 7 ms of the budget spare. A live bot match on Deck16 measures 74 to 90 fps.

Status

Where it's at

It's playable and I'm playing it. It isn't finished. Here's the honest split.