Tuesday, August 28, 2012

Portal 2 - Thaumaturgy Post Mortem



So many things just didn't work in this map...oh where to begin.

Cube Rotation:

First, I wanted the circuit cube to move to the same position as the cube only when placed in the button. This led to a set of 20 triggers, a func_tank, 6 info_targets, and several momentary_rot_buttons. It was promising until I couldn't get it to match exactly due to how func_tanks don't have a continuous motion when going past -90 (Up). They spin in place and this threw the momentary_rot_button that controlled the face spin off.

The next iteration was a bunch of momentary_rot_buttons for each axis parented to each other. First run didn't work out. I needed a way to dynamically set the parents to prevent orientation distortion. I put a func_rotating in, but they aren't reliable for exact rotations. Even when set to 90deg/sec and spun for 1 second, they slowly become more and more inaccurate because the "stop" command doesn't happen exactly 1 second later.

So, I decided to try scripting it all. After spending 4 hours trying to get a basic "hello world" working (you would not believe how little there is for vscripting tutorials. It's all "here's the reference and what I've done, have at it!"), I managed to get a button to execute a function (Using the console is unreliable. It seems to work more often when you actually have parameters to pass. Not having parameters results in errors because Source doesn't want to search for your PerfectlyCapitalizedFunction() and searches instead for PerfectlycapitalizedFunction() thus throwing an "It doesn't exist!" error).

Anyways, I setup a basic "AddDegreesToX(x)" function for each axis and put the script on a single func_brush. Once the brush was flipped on any two axes (180), adding or subtracting past 90 on the third axis would result in jittering. This jittering made no sense either. Why? My debug output was telling me -90 - 1 = -89 AND -89 -1 = -90. Same went for 90 + 1 = 89 and 89 + 1 = 90. Source refused to do proper math.

This led me to believe I had to do 3d programming in Squirrel. Let's just say that didn't go well. All the code I found was immediately broken because my initial angles were zero and 0 * cos(angle) - 0 * sin(angle) =, you guessed it, zero. The tutorials I found were all for manipulating the actual vertices of a cube with each face represented in matrices, not rotating a 3d model in a game engine. I never took 3d programming and, frankly, my head was spinning at that point.

However, that got me thinking I should use multiple rotators and just parent the circuit cube to the one I needed at the time. Well...it kind of worked. It appears logic_measure_movement copies and pastes angles, so every time I changed parents, the circuit cube was given an entirely different set of angles instead of just adding to its own.

I took a break from all that programming to go with a more visceral method of manipulating the cube: Walking into a projector that shows arrows that, when pressed, rotate the cube in that direction. The code was already there, I just had to build the button framework and implement player view/collision separation. Piece of cake, right? Well, I ran into that little issue of "when you rotate enough on two axes, you flip the third". This effectively reversed the function of the buttons on that third axis, not good.

So, I tried again, but instead of keeping the buttons stationary, I made them move with the cube's angles through another logic_measure_movement. It worked to a point, but after enough rotations, the problem came up again. Solution? Use the "rotating axes" idea to parent the buttons only to the axes they didn't rotate. So, X would rotate along YZ, Y along XZ, and Z along XY. While it looked kind of cool, it only made the problem occur sooner instead of not at all. Also, I was getting motion sickness from being a ghost inside a rotating cube. I abandoned the button idea at this point.

Next, I tried using a logic_measure_movement on a weighted cube, but I couldn't separate the translational data from the desired angular data. So, I went back to scripting and made a small function that copied the angles of a SourceCube and applied them to a TargetCube; no translational data transferred. That worked perfectly, until I changed the TargetCube from the model to a brush. Model-to-model worked fine, but model-to-brush rotated the angles by 90 on the Z (or is it Y? Whichever is up in Source) axis.

There was also this annoying bug where instead of setting the degrees exactly, 359.1 degrees would become -.9. so everything was a little off. I resolved this by going back to a model-to-model...uh..model, using a logic_measure_movement on the TargetCube and having it influence the CircuitCube. And that worked perfectly, again. Each rotation of the SourceCube was piped through the entities and correctly applied to the CircuitCube. So, the pipeline was SourceCube - > script -> TargetCube -> logic_measure_movement -> CircuitCube. Awesome.

The circuits:

I had circuits from my first "Circuits" map. They were alright, but I didn't like the fact I had the triggers outside of the instance and didn't really have a way to track the "flow" without explicitly setting one up. For a 3D cube where you can set many paths, this wasn't going to cut it. So, I dove in and rearranged the internals of the instances and added triggers and movelinears to function as flow control. If a trigger is hit, its corresponding movelinear moves up to hit the next trigger in sequence. This was doubled up so the circuits could be bidirectional if desired.

The new circuits worked like a charm except when it came to arranging them into a cube.

First, there was an annoying bug back when I had the cube parented to a momentary_rot_button. Each circuit's backing was a func_brush that was then parented to the center of the cube, a momentary_rot_button. It turns out "using" a func_brush parented to a momentary_rot_button will cause the button to rotate partially with respect to the axis selection. I turned on Developer 3 and ent_messages_draw 1 just to see where the erroneous i/o was coming from and there was nothing. No links whatsoever. The bug even persisted when both the brush and the button were set to ignore +use! This was eventually solved when I settled on making the CircuitCube's center a func_brush driven by the scripted movement measure system.
*Note: Parenting the brush to a physbox parented to the button did not produce the error at all*

Next, it turns out moving triggers will trigger with no apparent provocation when moving and they'll stay triggered until they hit a nice 90 degree angle (and sometimes, not even then). Needless to say, this was a big problem as it meant if you spun the cube right, you could glitch your way through the map. Sadly, this wasn't fixed. Filters don't even combat this. Sure, I disable them before motion, but if you enable them and they aren't at an angle they like, they'll fire their outputs.

I still didn't change the method of circuit rotation. I have a nice script to handle it and, since they only rotate on a single axis, it *should* work. Since I haven't released the map, I may still try it so you get a better experience.

And then I forgot to turn off the Afterburner OSD and had to record the run-through video twice.

1 comment:

  1. This is an interesting read for sure. Youtube algorithm strikes again.

    ReplyDelete