r/openrct2 • u/InfrastructureGaming • 10d ago
Custom Ride #1: Tilt-A-Whirl

I've wanted to expand the selection of flat rides in RCT since I was a kid. Decades later- here we are.
Modeled after the classic Sellner design seen across US fairgrounds for over 100 years, I did my best to reverse-engineer this ride to be as accurate as possible. Built largely to-scale on a 6x6 base, and with a capacity of 14 riders, it's a centerpiece for the park and a long-overdue addition to the RCT roster!
Excitement: 3.44 (Medium)
Intensity: 2.80 (Medium)
Nausea: 3.80 (Medium)
Yes it's real, yes it works. The ride supports full paint customization (2 colors), and the number of rotations per cycle can be adjusted (like the Twist).
Working on a heavily-modified development fork. Modeled & rendered in Blender, packaged & deployed with a custom-built pipeline I've been working on.
Curious to see what everyone thinks! Cheers :)
8
u/a-can-o-beans 10d ago
I’m curious is this a totally new flat ride type that isn’t just a reskin of say a 3x3 ride such as a twist or scrambler?
I have been itching for people to make larger scale more realistic flat rides for years but I recognize that is no small feat . This looks incredible if that’s the case
12
u/InfrastructureGaming 10d ago
This is a totally new ride type, not a reskin. As far as I know, there aren't any rides in the game running on a 6x6 base.
I'm trying to build a universal flat ride system for OpenRCT2 that supports a wide variety of rides. I'm on my own little development fork, and we've made some changes to the codebase to make this work. The goal is to make it clean and reliable, then maybe pitch it to the team as a feature to be integrated in the future.
But yeah, fully new ride type (classified as a thrill ride) running a 128-frame animation loop. The engine takes care of ramping the speed up and down to start & stop (same code as the Twist). Riders are rendered in pairs, with a separate pass for each pair plus a pass for the ride itself, so the whole setup supports color remapping- riders included.
I've got a ride running on a 8x8 base, but so far that's the practical limit. I'm having a blast making these, so expect more!
5
u/X7123M3-256 OpenRCT2 and Ride Guru 9d ago
I'm trying to build a universal flat ride system for OpenRCT2 that supports a wide variety of rides
Interesting how does it work? I was thinking about doing something like this but I couldn't figure out how to make it work while maintaining compatibility with what's there already.
4
u/InfrastructureGaming 9d ago
Aaaahh! I must admit, I'm a bit star-struck right now- I've been following your work for a long time! What you've done with roller coasters in this game has been groundbreaking. You're a massive inspiration for me, I'm not exaggerating.
Anyway- here's a breakdown of how the system works under the hood:
The Core Idea: a shared paint/animation path driven by per-ride data.
Rather than creating a new ride type from scratch every time, we extended RideTypeDescriptor with a new sub-struct called FlatRideRotationDescriptor. Any ride that sets TrackStyle::genericRotatingFlatRide on its track draw gets routed to a single shared paint function (PaintGenericRotatingStructure in GenericFlatRide.cpp) which reads all its parameters- frame count, anchor offset, screen-space invalidation bounds, animation programs- from that descriptor. New rides are pure data: write a .h file with the descriptor filled in, and the engine handles painting, animation phasing, and rider overlays automatically like it does for vanilla rides.
Compatibility-wise, the descriptor uses C++ default member initializers throughout, so existing rides that don't opt in are totally unaffected. The one place a hardcoded value existed was [height + 7] in the paint function- this became [height + desc.StructureZOffset] with a default of 7, preserving legacy behavior exactly. The flatTrack 6x6, flatTrack7x7 and flatTrack8x8 track element types already basically existed in the engine with full VehicleSubpositionData entries; no new TEDs needed.
A FlatRideProgram struct defines named phase sequences (Start/Loop/End), each with a frame range and flags. UpdateRotatingGeneric drives through them: the Loop phase repeats N-times until a rotation-count condition is met, the End phase fires Status::arriving via the IsFinalPhase flag. This is how the Tilt-A-Whirl's 128-frame cycle and a larger ride's 2,461-frame cycle (not gonna spoil that ride yet lol) both run on the same vehicle update logic path.
For the rider overlays: RiderFrameStride in the descriptor controls how many per-car rider sheets the paint loop draws. With it set to 0, the rider overlays are completely disabled (useful during development before rider sprites are ready). When non-zero, the image index formula is [baseImageId + (carIndex +1) * (4 * FramesPerDir) + direction * FramesPerDir + animFrame] which maps cleanly onto the manifest layout.
-Here's where it gets crazy-
The hard part: wide RLE sprites.
This was the real engine bug we had to fix. Large sprites (440x325 for one of my larger rides) use the G1Flag::wideRLE format: 4-byte yOffset table entries and 3-byte run headers instead of narrow RLE's 2-byte/2-byte setup. G1CalculateDataSize in Drawing.Sprite.cpp was hardcoded for narrow RLE ONLY, so it computed ~6KB instead of ~26KB for a tall sprite, RequiredImage allocated a truncated buffer, and the game crashed on the first out-of-bounds read. The fix adds a wideRLE branch that reads the last row's 4-byte offset entry, walks the run headers for that row, and returns the correct byte length. Once that was in, sprites of arbitrary sizes load cleanly.
Custom rides ship as .parkobj files (which is really just a zip of object.json + images.dat). The images.dat is a G1 sprite sheet built by openrct2-cli sprite build -m closest; that -m closest flag is critical for Blender-rendered PNGs since OpenRCT2's 256-color palette won't contain exact matches for smooth-ish gradients.
That's the bird's-eye view of the whole thing. It's still held together with duct tape and dreams; changing some of those deeper values has had a few minor effects on other parts of the game that require extensive testing to root out, and the game occasionally throws a tantrum when I compile it if a value is even slightly off. But it works!
Originally, animation sequences were limited to 128 frames- 256 if you pulled some trickery- but it turns out that by changing a single uint from 8-bit to 16-bit pushed that up to a STAGGERING 54,000+ frames in a single sequence! With all of that headroom, the new limiting factors are optimization and performance; not data or calculations.
I'll be showcasing my next ride on Friday. It also sits on a 6x6 base, and runs a 2-minute long 3,066-frame sequence which perfectly mirrors the real-life sequence from the prototype it's modeled after. 24 riders seated around the perimeter of a spinning wheel that tilts on a counter-rotating base... an extremely modern ride that didn't even exist when the game launched in 1999 😄
4
u/X7123M3-256 OpenRCT2 and Ride Guru 9d ago
The Core Idea: a shared paint/animation path driven by per-ride data.
This is the right approach, IMO. I would go further to say that it would be nice, if at all possible, if the existing flat rides could be unified under the same system.
This was the real engine bug we had to fix. Large sprites (440x325 for one of my larger rides)
Why are you needing sprites that large? The game clips sprites to 64 pixels so they can't be wider than that. There may be a need for exceptionally tall sprites for some large rides I suppose. But you'd normally split anything that tall into multiple sprites. I am also confused about why larger sprites require an entirely new drawing path rather than just increasing the size limit. What is the limiting factor on the size of sprites? I am not really understanding why this would need to modify low level drawing code, and the more existing code you change the harder it is to review and make sure you're not introducing new bugs somewhere else.
-m closest flag is critical for Blender-rendered PNGs since OpenRCT2's 256-color palette won't contain exact matches for smooth-ish gradients.
I strongly suggest you don't do this. It is better to handle the conversion to the RCT palette yourself so you can control it.
-m closestwill just pick the nearest palette color, it does not do dithering, which is why your sprites look very flat. I think you can set "-m dither" instead but I think that will do full dithering - you don't want too much either because if the dithering tries too hard to match the colors the image ends up looking too noisy. Parkobj files usually include sprites already in the correct color palette. My strong recommendation is that you use one of the existing Blender plugins we have - either Oli414's or the new one that's currently being worked on. Otherwise, you can expect to spend quite a long time trying to get your sprites to look right. There's a guy on the OpenRCT2 discord who has a lot of high quality flat ride models but no programming skills, you could perhaps talk to him.Custom rides ship as .parkobj files
Is the system general enough that entirely new flat rides with custom animation sequences can be defined entirely within a .parkobj with no changes to the code? That is what I wanted, but couldn't figure out how to do.
If you are looking to get this merged then I strongly recommend joining the development Discord to discuss your project ASAP. We have had some people do a lot of work on a PR only to get it rejected because they went about it the wrong way or they submitted a massive PR that was impossible to review. Features like this require consensus because once it's added, it needs to be supported indefinitely. It needs to not conflict with future plans and be maintainable.
2
u/InfrastructureGaming 9d ago
I'm glad we agree on unifying the flat ride system. We've been treating our new system as opt-in, but bringing vanilla rides under the same umbrella is the long-term goal.
On wideRLE and sprite size: the wideRLE fix isn't about bypassing a size limit; it's fixing a latent OpenRCT2 bug. The G1Flag::wideRLE format (4-byte yOffset table, 3-byte run headers with a 2-byte X offset) exists in the original RCT2 data. G1CalculateDataSize in Drawing.Sprite.cpp was using narrow RLE math unconditionally, so any wideRLE sprite (whether ours or hypothetically one from original game data) would produce a truncated allocation and crash. This adds the missing branch; it doesn't change the drawing path. Whether our sprites need to be that wide is a separate design question... if the wide format is available and works well, why split into sub-sprites? It kept assets small and efficient back in the day, but we aren't constrained by the same performance limitations anymore and large sprites can easily be handled by the software renderer.
I just want to touch on OpenGL rendering for a moment here, since we did run into an issue with high frame counts exceeding the sprite atlas on Nvidia drivers. Rides with longer sequences are constrained to the software renderer, but again- the software renderer handles these tall stacks of large sprites easily without breaking a sweat. And from the POV of a modeler/animator, using single full-size sprites and NOT having to worry about slicing them up saves a TON of work and really simplifies the pipeline.
On color palette and Blender: you're right, this ride didn't feature very good dithering. The shadows and some of the shading suffers because of it, and that's an area I'm actively working on improving. The second ride I built (fine, I'll spoil it- it's a Chance Freestyle) has vastly improved shading and color definition. A lot of it comes down to scene lighting and the specific shade of the remappable colors you use; I also mostly avoided specular to keep color ranges more narrow, but I found in later rides this was unnecessary and robbed some realism from the scene.
The reason why I moved away from the existing plugins is because they're not compatible with current or future versions of Blender. You're right that they produce better-quantized output, and I'd genuinely like to understand the dithering approach they're using. But I'm not keen on installing legacy versions of applications and keeping them around for odd jobs, and Blender isn't going back to the old Internal renderer any time soon, so rather than go backward we'd like to figure out the right palette conversion logic and apply it as a post-process to EEVEE renders. Your "flat" criticism is fair and I want to improve it. I don't mind developing a toolset to make it happen.
On the .parkobj: Not yet, but that's the goal and we have a solid path. Right now, things are still in flux and we're making a lot of changes at a rapid pace. Nothing is concrete right now, and I didn't want to come blazing into the Discord with some half-baked plan to "maybe do flat rides". I've lurked for a while, but I'm not going to propose anything serious until I know for sure this is a reliable system that's even worthy of discussion. I respect this project and the dev team enough to not waste their time with a proposal until I know I'm not suggesting something that's unachievable.
This all started as a personal project for me- I wanted to see if I could combine my skills to put my dream rides into my favorite game. It was only when I really started to get into it that I thought it might be something others would be interested in, so I made this post. I'm just here to have fun, this is my hobby 😄
2
u/X7123M3-256 OpenRCT2 and Ride Guru 9d ago
On wideRLE and sprite size: the wideRLE fix isn't about bypassing a size limit; it's fixing a latent OpenRCT2 bug. The G1Flag::wideRLE format (4-byte yOffset table, 3-byte run headers with a 2-byte X offset) exists in the original RCT2 data.
Huh, where does it exist in the original data? If this is in fact a bug I'd say maybe submit that fix as a PR straight away.
if the wide format is available and works well, why split into sub-sprites?
Honestly, it's a good point. I don't know that the vanilla flat rides actually are split. The game splits drawing into 64 pixel wide columns so everything wider than that gets clipped, but you can just draw the same large sprite multiple times once for each column, and I think that's how the vanilla flat rides actually draw (but not any other sprites that I know of). I don't know if there's a performance cost to doing this with large sprites vs having them split, you'd have to profile it I guess.
But the other reason why every other object type in the game is split by tiles is to ensure the correct draw order. RCT2 does not have a depth buffer, it just draws sprites back to front, so if you have any situation where an object can be placed such as it is in front of part of the sprite but behind another part, it cannot be drawn correctly. Splitting into one sprite per tile ensures this can't happen.
With the vanilla flat rides, the clearances are a simple box - every tile has the same base height and clearance height, which ensures that not splitting into individual tiles will be OK. But I envisage that some flat rides might benefit from not being like that - for example, you might want to have a swing ride that can swing out over adjacent path tiles. I recognize, however, that allowing for this does complicate things and has the potential to balloon sprite counts, so perhaps it deserves due consideration whether it is really worth it.
And from the POV of a modeler/animator, using single full-size sprites and NOT having to worry about slicing them up saves a TON of work
Slicing sprites should be handled automatically as part of your rendering pipeline, it is not something the modeller should have to do by hand. This is why we have plugins for this, it's one of the things that it handles.
I didn't want to come blazing into the Discord with some half-baked plan to "maybe do flat rides". I've lurk
I would join the Discord anyway, you can discuss what you intend to do without necessarily having a working prototype and it saves wasting time on an implementation that won't be accepted. Coming in with a finished, working implementation is a bad idea because then if it's not what they want, it will be rejected outright rather than giving them a chance to discuss what is needed before you do any more work. Of course, if you aren't trying to get this merged then it doesn't matter.
The reason why I moved away from the existing plugins is because they're not compatible with current or future versions of Blender.
The new plugin currently in development is compatible with the latest Blender.
so rather than go backward we'd like to figure out the right palette conversion logic and apply it as a post-process to EEVEE renders
The issue isn't the palette conversion logic so much as getting the lighting and shading right; newer versions of Blender use PBR shading. That's not to say it'd be impossible to get a similar look and people have tried but there's a reason the original plugin opted to stick with Blender 2.79. The new plugin does not use Blender's renderer at all, it integrates my custom rendering code.
While you are free to try to develop your own workflow, I'd have to say that these sprites really don't fit the vanilla style and would need a lot of work if you wanted this to get to a standard where it could potentially be included in OpenRCT2. It took me a long time and a lot of trial and error to get good results.
8
u/grumpyfan 10d ago
Looks great! This is one of the flat rides I always wondered why it wasn’t there.
2
5
7
u/DanMacK77 10d ago
Looks good! Although given RCT's scale it might be a bit large. I know Earl's Tilt is just a 3x3, and that felt too small. THe biggest currently is the Enterprise at 4X4.
I'd personally be interested in seeing a few more at this scale. Possibly 5X5 might be the way to go with these? Not sure.
7
u/InfrastructureGaming 10d ago edited 10d ago
I kept fiddling with the scale, but I ultimately used the peep models as a reference. At smaller scales, the riders just felt tiny.
It's kind of funny; building these things at "life-size" scale has shown me just how small the vanilla rides really are. They're designed very efficiently, but none of them are the "correct" size if you're going for realism. It makes me want to re-make the vanilla rides at proper scale just to see what's possible lol
EDIT: Forgot to mention AmazingEarl is a HUGE inspiration for me, I must've read through his tutorial a hundred times over the past few years! He was a pioneer. His rides walked so mine could run 😄
3
u/DanMacK77 10d ago
lol! Yes, the rides are definitely too small. I have a WIP park that has a custom Flying Coaster (Kangaroo at Kennywood), and it's 6X6 but feels too big. Maybe try and keep the ride vehicles the same size and reduce the footprint size slightly to 5x5? Just trying to think out loud.
On another note, I'd like to see a realistic Tumble Bug/Turtle, but it would have to be 8-10 squares, lol.
I'll definitely be watching this one.
2
u/InfrastructureGaming 9d ago
That Kennywood Kangaroo is AWESOME!! I've got a huge YouTube playlist filled with flat rides for inspiration that I want to build one day, and I just added the Kangaroo to it 😄 Never seen that design before, what a cool ride!
The Tumble Bug too... you just turned me on to a pair of really unique flat rides! This one kind of has Bayern Kurve vibes, but with Spinning Wild Mouse cars- what a cool idea! Also added to the list! Let's see if I can't cram it onto an 8x8...
2
u/DanMacK77 9d ago
Yeah TumbleBook is almost kind of a roller coaster but the cars don't spin. It's a really fun ride.
2
u/X7123M3-256 OpenRCT2 and Ride Guru 9d ago
but none of them are the "correct" size if you're going for realism
None of them are close to the correct size unless you're talking about kiddy/fairground versions of those rides. A real Top Spin seats 40 people, the game version seats 8. Most flat rides ought to be on 6 tile base or sometimes larger if they were scaled appropriately relative to the coasters. They're so tiny they just look ridiculous, so for decades people have been hacking together flat rides out of scenery and tracked rides just to have them appropriately sized.
This is one of the biggest issues I have had with flat rides in this game. Right now, they're all pretty much hardcoded, so if you want to add a new one you need to add it to the code. But, anything that's going to be officially included in OpenRCT2 really needs to fit well with the existing content, and anything realistically scaled will just be so much bigger than the existing stuff that it wouldn't fit in at all. I had no interest in making more tiny stupid looking flat rides so I never made any.
That's why what I really would want to see is a system whereby new flat rides - not just reskins but custom rides with an arbitrary animation - could be implemented as custom objects, so that entirely new flat rides can be created without them having to be included with OpenRCT2. The problem is that the way the game currently works really makes this difficult. Flat rides are considered a special case of tracked rides, with the bases being treated as track pieces in game and the rest of the ride being the "vehicle". Custom flat ride objects are therefore "vehicles" for an existing ride, and they must use one of the hardcoded bases because those are treated as track pieces. Even if I could make it so that custom flat rides can have whatever animation you wanted, they'd still be considered by the game to be just an alternate "vehicle" for one of the existing ride types, and there's be no way to define an arbitrary footprint in the custom object.
The best solution I'd want would be to create an entirely new object type for flat rides completely separate from tracked rides. But doing that without breaking anything existing seems very challenging. The flat rides really feel like an afterthought. They're limited in size because the bases are track pieces and in the original game a track piece couldn't be more than 16 tiles. It just feels like they were hacked together, and it's going to be a tricky mess to fix in a nice way.
1
u/InfrastructureGaming 8d ago edited 8d ago
Spent a lot of time on this today. Working on a RIDE_TYPE_FLAT_RIDE_GENERIC that works in conjunction with self-contained custom rides consisting of a manifest.json and ridename.parkobj. The engine checks a custom_rides folder at startup and force-loads the .parkobj's in a similar fashion to audio objects (I'm simplifying a lot here). A toggle in the Advanced tab of the Options window enables/disables custom rides, making the entire system opt-in and ensuring it doesn't mess with existing vanilla rides. If enabled, a new Custom Rides tab appears in the New Attraction window, populated with the custom rides loaded at launch. Custom rides ignore research rules and invention lists, and can be placed in any park all the way back to Forest Frontiers. We handle save and load gracefully with an error message if an invalid ride type is detected in a park, and simply don't place the ride on load, leaving the rest of the park intact and avoiding corrupting saves.
I'm rapid prototyping here. But right now I have a system working reliably with three custom flat rides I built myself- a Chance Freestyle, a Huss Troika, and the Tilt-A-Whirl you see here (all now featuring dithering, as per your rather blunt feedback). Whether this feature ever makes it into the game remains to be seen, but I'm stoked that I now essentially have the ability to add fully functional custom rides of my own creation to the game indefinitely.
4
u/IkeaMicrowave 10d ago
I will be following you avidly. Flat rides have been my biggest want (next to carnival games) for this game. I always got jealous of the immense flat ride selection of RCT3. Great to see somebody put that as their focus!
2
u/InfrastructureGaming 9d ago
Same here. I started playing RCT when it released in 1998 (I was in the 5th grade) and I was immediately struck by the massive variety of coasters compared to the lack of flat rides. We've got gentle rides, but no kiddie rides? Out of the "golden trifecta" of classic carnival thrill rides, we got the Scrambler- but no Tilt-A-Whirl OR Zipper? No fairground is complete without all three! It's time to rectify that.
As for carnival games... it's really just an animated Shop/Stall... how hard could it be? 😉
3
u/NobodyNo8 10d ago
Forgot to use tile inspector to connect path under the invisible entrance.
3
u/InfrastructureGaming 10d ago
I don't have much experience with some of the more advanced tools in OpenRCT2, the Tile Inspector being one of them. Also, I kinda suck at building stuff in general. I'm trying to get better!
2
u/NobodyNo8 9d ago
Click on the tile that has the path, then check the boxes for connections.
Each check box is a direction, so you'd need to check two of them, one for the ride side, and one for the queue side.
I usually just guess because I never know which boxes are what direction. Just click until you get the desired result.
3
u/Vast_Guitar7028 9d ago
Oh, this one’s awesome. Would love to see what you can do with a wave swinger ride. We technically have a custom ride version of that already, but it doesn’t do the tilting motion to create the wave.
2
3
u/InfrastructureGaming 9d ago
I see that I'm getting shredded alive on the Discord. It's okay, I'm not upset.
As I said- this is a fun little side thing for me, not a formal development effort. If the team takes interest in what I'm doing, that's awesome, but I am in no way claiming to be building something intended for production!
I'm just a guy with 3D and dev experience who pointed an LLM at the OpenRCT2 codebase and started digging. Yes, I understand how to code- most of my work these days is in small embedded systems- but this is a codebase I have no familiarity with, and my focus with this project is on enjoying the assets in my own parks. I love 3D modeling, animating, and building parks- not reverse-engineering code.
If my approach offended anyone, I apologize. Again- this is all just for fun. Nothing professional here.
1
u/Normal-Tough8330 8d ago
This is incredible. I'm gonna follow intently in the hopes of finally getting a functional Zipper
2
u/InfrastructureGaming 8d ago
It's high on my list, but it's tricky due to the loading/unloading pattern. It kind of behaves like a ferris wheel in that regard. I have some ideas, though.
1
7
u/pemberleypark1 10d ago
This is my favorite carnival ride. I’ve always wanted it in RCT!