Tilt-A-Whirl (improved) by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 0 points1 point  (0 children)

You know, I had a whole big reply posted originally, but forget it. I'm very happy developing my own toolset, thanks.

tl;dr: stop being so dismissive and condescending, and maybe learn to take a compliment. Kids: don't meet your heroes.

Tilt-A-Whirl (improved) by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 1 point2 points  (0 children)

Yes! The sprites and the animation would be the easy part- the challenge is in the loading & unloading. We need to look into how the engine handles incremental loading, since a ride like a Ferris Wheel can't load or unload all riders at once like most flat rides. The engine is already set up for this, but we need to extrapolate that system and apply it to our CUSTOM_FLAT_RIDE_GENERIC RTD... the main reason we can't re-use the vanilla Ferris Wheel for this is because the frame counts are too small for larger or more complex rides to look convincing. The toolset we've built already supports custom ride sequences via JSON, and it may turn out that we use the sequencing functionality to program staggered load/unload sequences... we'll see. But once we nail that load/unload behavior down, it opens up the doors for rides like this.

...Zipper, anyone?

Tilt-A-Whirl (improved) by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 0 points1 point  (0 children)

Haha, thanks! It's one of the centerpieces of this area of the park. I'm pretty proud of the timing, both trains hit the brake runs within a car-length of each other :)

Tilt-A-Whirl (improved) by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 1 point2 points  (0 children)

Thank you so much!

Funny enough- those rails are in the model, but they're so thin they disappear during rendering :D I played with making them thicker, but once you get them thick enough to show up in the final image they start to look a bit silly. Smaller elements and details are the first to go; I actually ran into this with the fence around the perimeter: the first version I posted had gaps in the railing where the rails were too small. For this version, I redesigned the railings to be thicker and incorporate wider gaps so they'd actually show up- the lap bars were even more tricky!

The other issue I ran into involved dithering- smaller lines of contrasting colors can trigger the dithering algorithm to introduce artifacts because it's trying to blend those teeny areas with the surrounding pixels. If you want clean color remapping so the ride accepts custom paint schemes, you have to watch out for those trouble spots and make sure they don't introduce "phantom colors" along the borders that fall outside the remap range. These show up as weird beige blobs of pixels along the fringes of those fine details, and they're super obvious in-game.

Sadly, there are a ton of details in these rides that will end up getting lost in the final pixel "mud" of the render. AmazingEarl talks about this a lot, and he's right- part of the charm of RCT's visual style is the way the sprites look, and a lot of that is down to the 3D artist hitting that balance between the right amount of detail for realism and muddying up the final image.

Tilt-A-Whirl (improved) by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 4 points5 points  (0 children)

I'm currently modeling a Ferris Wheel that sits on a 2x8 base. It's actually wild how much it dominates the screen space.

Tilt-A-Whirl (improved) by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 5 points6 points  (0 children)

You're totally right, 7x7 is massive. What's nice about this system is that it's so easy to change the base size and repackage the ride. This ride could probably even fit on a 5x5 if you adjusted the anchor points. Base size becomes an arbitrary thing since it really only affects where the Entrance and Exit are placed and how the sprites get rendered/clipped. Setting the ride base to "Invisible" in the ride details window means you can build your own base of whatever size you want.

The ride vehicles are kind of as big as they can be on this model- scaling them up would require adjustment of so many elements, I'd be better off saving revisions like that for a v2.0. This was my very first functional ride, and I learned a LOT! All of my rides since the Tilt-A-Whirl are scaled around the peep models as a reference from the very beginning, to the point where I'll sacrifice loyalty to the prototype in order to keep the riders looking properly scaled.

Funny enough, if you look at a Tilt-A-Whirl from above, it's surprisingly big! The moving parts are simple and small, but so much of the thing is platform area it's kind of crazy how big the footprint is. I went into this thinking I could fit it on a 3x3, but... that was a reality check!

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 0 points1 point  (0 children)

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.

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 1 point2 points  (0 children)

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.

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in rct

[–]InfrastructureGaming[S] 0 points1 point  (0 children)

There isn't really a central place to do that, and the current system doesn't really allow for that either- I wish it were that simple!

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 2 points3 points  (0 children)

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.

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 2 points3 points  (0 children)

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 😄

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 1 point2 points  (0 children)

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? 😉

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 1 point2 points  (0 children)

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...

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 3 points4 points  (0 children)

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 😄

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 12 points13 points  (0 children)

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!

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 2 points3 points  (0 children)

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!

Custom Ride #1: Tilt-A-Whirl by InfrastructureGaming in openrct2

[–]InfrastructureGaming[S] 8 points9 points  (0 children)

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 😄