Devlog #002+a - Silkier Sutures (Patreon)
Content
The sutures really sucked.
But you see, had I known that, I wouldn't have released the demo with said sucky sutures. Suturing is a really common action in CE:ROM, so you'd want to make them good, right?
Sutures I did for the trailer. Works for me!
Well, on both my desktop and laptop PCs, it seemed fine. I'd used the sutures countless times while developing and testing, and felt natural and easy to use. No problems, right?
What is that.
I mean, the player does do a /\/\. I guess it was lagging? Hmm...
Excuse me what.
So, uh, it turns out that the lower the FPS is, the crappier the sutures become. Now hold on, don't take out your pitchforks just yet! Most of the game's logic runs on the physics_process step, which occurs a (mostly) stable and consistent number of times per second.
However, since sutures are highly-dependent on the position of the cursor, I'm instead opting to use the process step, which can run even more times per second. That way, I can get as many samples as possible on which to run the suturing algorithm.
I usually run at 144 or 60 FPS, so it wasn't really much of a problem.
And then, the demo feedback started coming in.
"The sutures are really hard to use", "Sutures seem to work at random", "Suturing feels really hard", "At lower FPS, the sutures are much less reliable", "The sutures feel like they don't connect", "Using the sutures is kinda miserable", "Suturing is a bit awkward", ...
Ah. That's not good.
I figured, yeah, okay, I should probably look into it.
Identifying the problem
I like visualization tools, so the first thing I did was code up a quick way to see exactly what the game saw. I made it so that every time the game got an update about where the cursor was, I'd drop a circle. Cool, done.
Then, I loaded into my dev sandbox and tried doing some sutures...
Circles are dropped as fast as possible where the cursor was. Diamonds indicate the game decided a new "zig" in the "zigzag" should begin.
What the hell is this. This is frigging awful! "Luckily", I was using my laptop due to travel (which was mysteriously running the game at 40 FPS, much lower than I'm used to), so I was able to test the game at a laggier speed, but wow. I didn't realize it was THIS bad.
Well, at least I could see what was going on. Time to fix it. But first, a quick tangent.
Stride detection
Let's call each white line segment a "stride".
To suture up a laceration, you'll want to make a zig-zag pattern, like above. Cool, seems simple enough. But how do you detect this?
Well, one way is to just make lines between every single point you read from the cursor.
The problem is you now have like, 100 lines. We're really only interested in the overall shape they make and whether or not that shape looks like a /\/\/, which would be only 4-10 strides or so. So we need a way to detect when to make a new stride; in other words, when a / should become a /\.
I toyed around with a lot of different methods, including:
If the angle changed enough. If you're drawing a straight line, your heading should remain about the same, and therefore if you "change direction", your heading should also change. But this sucked, because in reality your mouse can jitter around a bit, ESPECIALLY when you've slowed down. Calculating some sort of 'rolling average heading' seemed cumbersome and not very flexible, either.
If your speed changed. If you're about to change direction, you'll probably slow down. But this also sucked, because it became really FPS dependent, as doing it on the physics tick was too slow. Plus, if you were to pause or go too slow, a new stride would start, resulting in a sort of jagged but still mostly-straight line.
Some kind of combination. Sucked! Even with deadzones, depending on angle or speed was really unreliable.
I'll spare you the details and instead just tell you how the game detects a new stride:
If the cursor's position starts getting closer to where it started, a new stride begins.
There's a bit of nuance (ex: a deadzone), but it really was that simple. All I had to do was keep track of where a stride started (which I was doing anyway), measure distance, remember it, and compare it with a bit of deadzone. That's all. Easy!
A fun trick: I don't even use the real distance. I use distance squared, since square root is an expensive operation.
Back to making sutures suck less
Anyway, I quickly realized that the "node" chosen to start a new stride was off by one. Specifically, it was one "too late".
Remember what I just explained with detecting a new stride? Turns out, the node I was using as the endpoint for a stride was the node that is considered "getting closer" to the stride's start point. Oops. I needed to use the node prior to that. An easy fix, thankfully.
Hmm... much better, but not quite. For some reason, a stride would sometimes do a Cool Thing and not actually work, resulting in a stride being "missed" even though you did a zig-zag. (See how, in the above image, there are red dots on the bottom right? That's where the mouse was, and the game saw it, but for some reason it didn't register.)
Okay, something weird is going on. I updated the visualization debugging tool to try and help. The main thing to look at are the red lines: they point towards the "start" of a stride, which makes it easier to tell which ones are being considered when looking for when we're "getting closer" to the origin.
I also added some text and numbers.
Ohh, I see. Do you see it? That stride in the middleish, the one that looks like a plateau?
There's not a red line connecting it to the origin. In fact, the next one doesn't have one, either! Also, the numbers look weird! Why's the bottom dot say "36.06" when it's clearly very far from where it's pointing at? Another dot that isn't much closer says "168.63"! What gives?
Uhh yeah, the origins were being assigned incorrectly. The game wasn't detecting a new stride had occurred because it was being fed incorrect data. That shouldn't be too hard to fix...
Ahah! Success! No more janky sutures!
It even works when going fast. (See how there are fewer dots? Taking more time by going slow means the game gets more points from the cursor's position. Going fast means it has way fewer points to work with.)
Great! The sutures work way better now, job's done. Easy! (It was not easy.)
Also, fun fact... my laptop was actually in some sort of "fanless" mode, which throttled the game's performance. Changing that back to normal pumped me back up to 60 FPS. But it was a blessing in disguise, since it let me experience the pain a lot of other people were.
The Sutures are ready
Anyway, these new and improved sutures are already out in the updated demo. (See the patch notes here.) Go give them a try!
alt::Devlog #002
• Updated Suture scoring algorithm to impose a slight penalty for zig-zags that are too far apart.
For those of you with paid tiers, I have a bonus section explaining this patch note...
Nerfs? In my sutures?
This really is the gift that keeps on giving, isn't it?
So there's still a problem. With clever movement, you can still draw a suture that looks like this. And it counts as a COOL+50, which is max. Yeah... uhh, not ideal. I should fix that.
I'll explain how sutures are scored in a future post, but for now, you just need to know two things. A suture is looking for:
Coverage - basically, what percent of the laceration did your sutures cover?
Quality - basically, did your strides actually cross the laceration, and if they did, were they of reasonable size?
The pictured COOL+50 suture actually has 76% Coverage, and its Quality is 75%, making it literally just qualified for maximum points.
That's jank! I can't have this! I refuse! Time to fix it!
What's the problem?
Coverage was the thing that was meant to address this. See, with Coverage, I could make sure that stuff like:
-/\/\/-------
would be considered bad, since hey, you missed like half the wound!
The problem is that Coverage only looks at two intersections - the closest one to each end of the laceration.
-|---|-------
So, in that pictured COOL+50, you can see there are only three intersections, two of which are super-far left, and the other pretty-far right. As far as this algorithm is concerned, that covers about 75% of the cut. Gold star!
|--------|--
Well, there's a way to fix that. If what I'm going for is roughly-even strides (/\/\) that cover the wound, then I could look at the distance between each of the intersections.
Brainstorming time
To figure out the specific algorithm, I made some examples. I spaced out x's on a line, representing the intersections in our hypothetical laceration. There are three here, starting in the top-left corner: "Ideal", "Jank", and "Missed a step".
I then took everything I could measure to see what data I had to work with. For example, in the "Ideal" case, we have a bunch of intersections with length between them 3 (I just made it up), and a total length of 15. For "Jank" it went 1-1-13, and for "Missed", 2-7-3-3.
Then, I could calculate things like the difference between these values and the "ideal" value (for evenly-spaced sutures, it would be total length divided by number of segments), their averages, etc. etc.
Homing in on the algorithm
Then, it was just a matter of playing around with the numbers I had and could compute until I found something that felt right. Remember: the goal was to slightly penalize sutures that had "missing" intersections.
Here, "delta" is "how far off" the stride is from the ideal size (lower is better, 0 is perfect as it means no difference). To summarize, I hedged my bets between using the average of deltas and the worst delta. I averaged these two values, and if they were too high, I would start imposing a penalty to the suture's final rating.
This also had the neat side effect of making POORs more common. And jank sutures would no longer report as COOL. Good enough for now, mission accomplished. Time to move on.