Lasers, Procedural Meshes, Separation of Concerns


Once I set up the project and had the managers in place, I started working on gameplay. The puzzles in Colorstream are focused on a 'stream' - it's in the title. In terms of narrative and design, the stream represents pure light in the context of the game. In the game's metacontext, the world exists inside someone's mind, and the streams represent a passion or a part of their personality. In terms of functionality, it's a laser system. The system needs to be dynamic, so when it hits an object, that object can affect the stream from that point on. This will also help with separation of concerns, so instead of the stream handling every possible case and the objects just using tags or something equivalent, the objects will contain their own functionality. Without going on a rant, I think separation of concerns is very understated, in game development more than anywhere else. When you violate separation of concerns, you're almost inevitably lead to a decision between repeating large sections of code, making that violation even worse, or completely rethinking the system.

So to start, I needed some method for calculating hit points. Fact: there needs to be a maximum point count, otherwise it's possible to get into a situation where the stream is reflected in an infinite loop. Since a static size container is more performant than a dynamic size container anyway, I decided to store the points in an array and calculate the points in a simple for loop with the possibility of an early exit. This means there can be garbage elements, so when iterating over the array it's important to use active count instead of the array length. The calculation itself is straightforward. I have some 'rules' that control the maximum length to trace, and then it's a simple raycast. If the raycast succeeds, I add that point to the array. Then I get all implementations of a stream interaction on the hit object and call a method for when hit by the stream.

That's all pretty simple, but it isn't dynamic - not in the sense that hit objects can affect the stream. This is where the stream context comes in. The context is a struct that contains data relevant to generating a segment of a stream. For instance, the ray being cast, the ray of the hit (hit position, hit normal), and color, to name a few. The context can be passed by reference through the interface method and modified by any hit object implementing that interface. An object can act as a filter and alter the color or it can act as a mirror and reflect the direction around the hit normal. It can do anything, really, as long as the relevant data is in the context. The interface method can also give the system custom information about the hit, such as whether it's blocking, whether the hit object should be ignored for the next raycast, etc. Finally, I used the point data from gathered from the traces to create a procedural mesh. I won't go into detail about that because it's all pretty straightforward.

All in all, I think it's a very powerful system. It's very dynamic - both in the sense that objects can alter the stream and that the mesh is generated at runtime. The only issue in all of this is it's a bit of a performance hit. I haven't done any profiling or optimization, but it's still playable, above 60 fps. And all I have to do to create a new component that modifies the stream is to implement a single method interface and change the variable that matters. The object-specific functionality is handled by the object, and the stream-specific functionality is handled by the stream.

Leave a comment

Log in with itch.io to leave a comment.