Helios was asked to build a Virtual Reality experience for SXSW 2016 which had a physical integration – a bicycle! While riding a physical bicycle and wearing an Oculus DK2 headset, the user pedaled a virtual bicycle down one of three paths set in an Austin, Texas like environment. The project had a short six week turnaround, and the client desired a realistic looking experience, both in the physical and virtual world. On the software side, we chose Unreal Engine 4 as our engine of choice due to the ease of building experiences and the amount of built in tools and content available. On the hardware side, we chose a RacerMate CompuTrainer Classic. The client had expressed a desire to preserve the look of a standard bicycle, so other training bicycles were out, and the CompuTrainer has a Windows API available via an included serial to USB adapter.
The experience used the CompuTrainer Classic to read the speed of the physical bicycle as well as setting the resistance on the bicycle. As the user went up and down hills in the virtual world we either increase or decrease the resistance they were experiencing, making it harder to pedal up hills. We also implemented coasting using some simple logic which checked the pitch of the bike to give the users a realistic experience if they decided to stop pedaling while going down a hill. The virtual bike moved on a rail and did not allow any turning. To do this, the virtual bike simply followed a spline and the position along the spline was incremented each frame based on how fast you were pedaling the physical bicycle.
The client had requested three different routes with different environments, but similar background elements. Due to the short 6 week time period, we opted to build all three routes into the same overall map, which allowed us to share the background pieces between the routes easily. On a technical side, we used a Persistent Map in UE4 to hold ‘global’ information such as lighting, skybox scenery, and other things shared between each map. Within that, we created two sub-maps per course which were loaded on startup based on the settings for that particular machine. There was an “art” submap, and a “logic” sub-map. This workflow was designed so that ideally the artists could be modifying the art of the course while a programmer worked on any logic related to the course such as the track, environmental triggers, etc. This did not go as planned. Due to the time constraints the artists often ended up being the ones who needed to modify and update some of the items from the logic layer as they tested and updated the route that the bikes would take. Because of this, we wasted some work where both the artist and programmers had modified the same map and someone’s changes had to be lost.
Technical Caveats
- The CompuTrainer SDK filters the data it receives from the hardware, so the data you received is not raw data. In reality, this means that value changes aren’t instantaneous and run one to two seconds behind what the bicycle is actually doing as it slowly settles on its new value. This is really only noticeable when you slam on the brakes.
- There are two types of RPM (cadence) sensors, both have pros and cons.
- The standard cadence sensor is a magnetic sensor and needs to be mounted on the bicycle, with a magnet added to the pedal. In reality, this turned out to be very fiddly as there is a low tolerance of acceptable distance for the magnet to be from the sensor as it passed by. We did not have the time to try any variations (such as a stronger magnet) on the standard hardware.
- The other option is the optional optical cadence sensor. The required positioning is less exact as it sits on the floor underneath a pedal, but it can get kicked by the users getting on and off of the bike. There were no obvious ways to secure it to the floor, so we opted to use the magnetic cadence sensor instead.
- The CompuTrainer API is 32-bit only. We thought this one was going to be a big hassle, but thanks to Unreal Engine’s excellent build tools this turned out to be less of an issue than we thought. Unreal Engine’s editor is 64-bit only however, so this cannot be used inside of the editor, only in 32-bit builds. We created a workaround so that testing could still be done in the editor using a standard keyboard.
- Initialization of the CompuTrainer hardware is slow, 3-4 seconds. Our solution is not threaded, so you will stall your game for 3-4 seconds when you try to initialize the hardware. We initialized the hardware once in the Game Instance which moved the long boot time to the start of the experience, instead of on map load which made it a non-issue for us.
- CompuTrainer SDK is not provided in our code, as you will have to request it from RacerMate. Rest be assured, it is easy to integrate once you have it!
Initializing the CompuTrainer via the GameInstance in UE4.
Implementation Details
- The SDK integration is done using native C++ with a custom Unreal Build tool script to statically and dynamically link and include the required libs/dlls/headers. See CompuTrainerModule.Build.cs.
- The Unreal Build Tool lets us create a #define based on whether or not the CompuTrainer is supported on the current platform. This allows us to use #if blocks in the actual C++ implementation to differentiate between Editor and 32-bit runtime. See CompuTrainerModule.Build.cs again.
- Values from the CompuTrainer are sent to Blueprint (where the main gameplay code lived) using a callback every 0.5s. These values are sent through the same callback regardless if you’re using the debug functions in the Editor or using the actual hardware during runtime. This simplifies the Blueprint code and makes the Editor a more reliable way of testing the experience. See CompuTrainer.cpp implementation.
- EDITOR ONLY: The Pawn polls the user keyboard for the up/down keys and feeds this into the debug speed up/speed down functions on the Module. This code ships with the game, but is stubbed out at runtime so we don’t worry about someone accidentally pressing the keys.
- Due to long initialization times, we specifically call the “Initialize” function when we want to initialize the hardware. The CompuTrainer takes about 3-4 seconds to try and initialize so we controlled the initialization time to try and hide it in-game.
Reading data from CompuTrainer.
What We Learned
- The CompuTrainer does not support hot-plugging of cables officially. You run the risk of blowing a fuse in the power supply if you plug/unplug cables while the device is turned on. We did this on accident during development and blew the fuse, leading to us rushing to find a replacement fuse so we could continue development.
- A standard 1A 250V Slow-Blow fuse will work. Fuses however, must be soldered in. There is no fuse holder inside the power supply, and instead you must unsolder the old fuse (or clip it off) and solder in the new one.
- There are a lot of wires related to the various sensors on the CompuTrainer on a bicycle with spinning wheels and pedals. Take your time and ensure cables are properly secured down and that you have no loose loops which can be caught by a foot or wheel.
- The CompuTrainer had to be physically tightened down periodically due to heavy use. We had almost constant use throughout multiple days so they experienced a lot of people getting on and off the bikes.
- There is a maximum width and radius to which tires will fit. We had a lot of trouble with non “racing/street” style tires being too wide and rubbing on the CompuTrainer frame. This increased the resistance too high to calibrate the CompuTrainer and the tire had to be swapped with a smaller one.
Changing speed in the Editor
Overall
During actual development, we were surprised to learn that SetErgonomicMode did not work as we expected. Using SetErgonomicMode does not provide a consistent resistance experience to all users. When we first implemented it, we thought that it was an added resistance. However, we discovered through practice that it’s the required output from the person pedaling. This means that if a person is pedaling slowly and putting out say, 25 watts of power and you set the resistance to 100 watts then they will experience a severe spike in how difficult it is to pedal. Alternatively, if they’re already pedaling at a fast clip and outputting 75 watts of power and you set the resistance to 100 watts then they barely notice an increase in load. We did not come up with a solution for this one. One choice might be to look at using Slope mode, or to sample their current output in watts when they approach a hill and make the hills additive, not absolute.
The CompuTrainer SDK will not provide a speed in KM/H unless a cadence sensor is plugged in. The cadence sensor doesn’t have to report valid values (ie: be properly attached to the bike), it simply has to be plugged in. When the CompuTrainer is running in standalone mode (ie: not connected to a computer) it reports the speed in KM/H both when the cadence sensor is plugged in, and when it is not. This indicates it is most likely a bug with their Windows SDK – we reached out to them but did not hear back about it.
The code for CompuTrainer Module is available on the Helios Github. Included in the repo are instructions on how to integrate the CompuTrainer SDK files (dlls, lib, and .h), as well as instructions on how to integrate the Module into UE4. As a word of warning, we did not expose all functions of the CompuTrainer SDK. There were a couple more that we didn’t have any use for, so we skipped them! If you need access to them, they should be fairly easy to wrap based on looking at the existing implementation. Simply use an #if guard around the actual code and provide a stub that returns similar for when it’s running in the editor.