Arc trajectory bug fixing, unit tests and MapMagic tangents

Wednesday morning and Saturday work.

Investigating MapMagic — Wednesday’s Unity Asset Store Daily Deal.  Looks like a super procedural generator — both Editor and runtime.  It outputs to a Unity Terrain but the forum suggests it could go otherwise.  I’ve always wondered about a single-player mode that places interesting things at points in an otherwise procedurally generated world.

Decided to buy it.  Lost a few hours to fiddling with it.  Man!  Messing with procedural generation and terrains is a surprising amount of fun!  Kind’a tempted to drop the asset straight into SnwScf project and hack-up a single-player mode.  Not now!  😉

Noticed an oddity: A rolling snowball hitting a snowman hits assertion failure:

SnowballShot !spawned but being destroyed. Not merely done twice in same frame, now:8504, spawnedChangedFrameNumber:8502

Not a problem but indicative of some mistake / mistaken understanding.  Have to come back to this since…

Darn, while capturing screen-shots for this post, I’ve just noticed the aim-assist going completely wrong!?  What looks like a straight shot is actually doing a lob, arcing waaaay over the top of the target.  Great 😐

Screenshot 2016-05-25 09.59.39.png

“Excuse me old bean, I don’t mean to criticize but I’m down here.”

Oddly it really is fine most of the time — I can only seem to prompt this when I’m trying for a screenshot!?  Riiiiiight.  Hopefully it’s something related to pausing the Unity Editor but I can’t really trust that without investigating.

Sigh, I thought that functionality was all sorted.  Guess I need more integration tests … ok, admission time:  By that I mean /some/ integration tests on this area 😦  They broke when I upgraded Unity most recently and I haven’t invested the time to resolve what happened.  Shame on me.  That is one of the downsides of my integration tests — they feel rather too brittle and aren’t runnable without jumping to the integration test scene.  OK, probably the better answer is that this area should be unit tested rather than as well as integration tested.)

So, I did warn this was going to basically be my dev notes.  Often they’re high-level discussions but, “when the going gets maths, the notes get mathsy”.  Erm?  Fair warning: Here be details!

Let’s get some values in here:

Shooter transform:
  pos: 11.05, 0.75, -0.23
  ori: 4.22, 90, 0

Target transform:
  pos: 16.11, 0.64, 0.53
  ori: 355.36, 155.00, -0.00

Targeting logging:
  (targetPos:(16.1, 1.6, 0.5), rbl.velocity:(0.0, 0.0, 0.0), transformToAffect.pos:(11.1, 1.5, -0.7), bulletSpeed:9.857765, shooterVelocity:(0.0, 0.0, 0.0), shotObj.aimingShouldConsiderGravity:True, maxTTL:2)
  => timeToImpact:0.5230598, launchVector:(4.8, 8.5, 1.2)

Obviously launchVector y component 8.5 is causing the lob over the target’s head.  Either enable more detailed logging or debugger.  Going with latter.

targetPos = "(16.1, 1.6, 0.5)"
bulletPos = "(11.1, 1.5, -0.7)"
Follows the "stationary target" code path.
Follows the "gravity-compensating projectile lob" code path and calls my ArcTrajectoryUtils.getLaunchVector() code.
planeVectorToTarget = "(5.0, 1.3)", range = 5.15528f, height = 0.0974731445f
then does getLaunchAngle(range, height, projectileInitialSpeed, gravity), i.e.
getLaunchAngle(x = 5.15528f, y = 0.0974731445f, u = 9.8219f, g = -9.81f) -- this might be the part that needs working, perhaps use as unit test?
for +/-, minus = -58.2228546f so used plus = 77.86665f.
Result from getLaunchAngle() angle = 1.04258835f.  Hm, that's radians = ~60 degrees elevation!
Just to carry this through, it then calls convert3SpaceScalarsToVector(vectorToTarget = "(5.0, 0.1, 1.3)", projectileInitialSpeed = 9.8219f, angle.Value = 1.04258835f)
Eventual launchVector = "(4.8, 8.5, 1.2)"

So, looks like there’s an error in getLaunchAngle(), perhaps its use of +/-.  Unit test time.  Also it’d be nice to get a code path for working situation — is it that the ‘minus’ case always works  when used and the ‘plus’ case always fails when used?

The formula it’s using is from this gamedev.net post.  It’s working well most of the time.  Hmm.

Added unit test for all this code (ArcTrajectoryUtils).  Also added unit tests for general ProjectileAimingUtils.  For the latter, I also wrote an IEqualityComparer<Vector3> so I could use NUnit’s constraint-based “EqualTo().Using()” testing approach with an epsilon value (i.e. the allowed proximity).  (I find it odd that C# doesn’t allow inline implementation of interfaces — had to add an actual concrete implementation to my VectorUtils!?)  Anyway, it allows this:

assert.That(v1, Is.EqualTo(v2).Using(VectorUtils.buildVector3Within(epsilon)));

Covered the area in unit tests.

Ahha!  Found the problem — I’d slightly mistranslated the equation!  Bracket in the wrong place was giving the wrong result in some cases!  Fixed and committed!

Since it’s a bank holiday weekend here in the UK, I’m calling Saturday an early night to pick up for something more fun than bug fixing tomorrow — maybe more fire?

p.s. since I was lost in the depths bug fixing (and parenting) all #ScreenShotSaturday, I tweeted this and was kind’a pleased it got as many Likes as most of my SSS pics!  I get the strong impression all gamedevs wish they could do proper software engineering with things like testing, etc 😉

 

AI getting stuck, part 4

Continuing from yesterday’s work, Justin PM’d me back about the event finding code saying it finds parent class ones as well.  Checked.  He’s right!  He said he’d investigate better way to implement that will fulfil both needs (not finding the version from Task but finding both leaf and intermediate implementations).  Feeling bad (and curious), I decided to investigate as well.  Found a simple solution.  Posted solution to GitHub.

Ported changes back to SnwScf actual.  Looks good.

Lunch then swimming lesson with 1.5 year old this afternoon.

Sadly ill in the afternoon and early to bed.  Ill again today but gonna struggle on.

Gonna let myself off the hook from posting this blog post and finish it today — Bank Holiday Monday in the UK 🙂

Right, where were we?  Oh yes, the dev version keeps hanging due to an Assertion failure of mine — trying to start a Shot’s Coroutine when the Shot is inactive?!  Hm, I wonder if its Collider is hitting something on the frame of its creation — perhaps the prefab should have disabled Collider which is enabled during shoot()?  Investigating.  So gonna need to account for setSelfHit() not working when Colliders disabled.

Also, the Snowman AI likely needs to use different PID values for rolling vs. sliding.  It seems to work OK for now but, once I get that assertion failure fixed, this will probably be revealed as ‘feeling’ wrong.  It’ll probably look like it moves too slowly since in the AIDev project, the rolling defaults (0.25, 0.225, 0.4) didn’t feel good whereas a simple (1, 0, 0) actually seemed OK … but won’t handle external forces, etc.

Had an idea while investigating the assertion failure:

Idea: Logging that is silent until assertion failure

Inspired by DTrace, I wonder if one could contrive a combined logging and assertion system where one logs as much as one likes but nothing is output until an assertion failure (or other event).  Obviously, there needs to be zero production-build effect and, in the Editor, there’s minimal effect until needed.  There’d be configuration to declare for which things this is enabled at which times.  As example, perhaps you leave it disabled by default then when on seeing an odd exception, you crank-up the dials to enable it on everything and wait for the exception again at which point all relevant detail is dumped.  Interestingly, I have most of this already in my logging and assertion systems.  I just need to tie my logging into a per-origin StringBuffer which the Assertion framework would need to pull from.

In fact, I wonder if one could even retrospect it atop Unity’s logging (since Unity exposes Log.logger)? Probably not without one’s own Assertion library *and* some concept of logging origin.

Anyway, back to investigation.  Yep, it’s colliding and being destroyed.
Relates to how I enable and disable Colliders and their interactions during firing.  I currently have:

  • currentlyPreparingShot.setSelfHit(false) — which prevents a Shot from hitting its owner.
  • startSelfHitCoroutine() which starts disableSelfHitCoroutine() — together they make the Shot enable its Collider’s interaction with the owner’s Collider shortly after launch.

I’ve now changed it so the Shot’s Collider is disabled in OnSpawned() (which might not affect some unpooled things like that reside in the level, e.g. SnowFallers — snow that drops when disturbed).  It is then re-enabled in onBeingFired() and in startSelfHitCoroutine().  (might be double-call but doubt it’ll cause problems.)

Yep, seems to fix 2 bugs in this area I was seeing.  However, this is one of those areas with odd subtitles, timings and interactions.  Time for a run of the integration tests!  (yes, I rather wish they could be auto-run in the background after compile!)

Not done integration tests yet because manually testing Shovel and WetFishSlap (you do know those are weapons in SnwScf, right?) revealed a couple of easy fixes.  Done.

Dinner time!

OK, committed all those changes.  I can now see I was definitely right about the PID values for sliding vs. rolling.  Gonna need those to change mid-use.  In PID Controllers, this is apparently called “Gain scheduling“.  Whether it should retain sets of values to interpolate/swap between or simply be assigned a new set of values?  YAGNI: I’ll try the simple option first.  I wonder if I could just swap the values over directly?  I did have the PID values as readonly.

Added OnRollStateChange event to SnowmanRoller which an SnowmanAIController subscribes to.  Added PIDControllerSettings to encapsulate changes.  Nice and neat.

Question: C# Generics [optionally overloaded] method constrained to either type or interfaces?

Hmm, just came across some of my generics code where I hadn’t constrained the type down and I really should have.  Oddly, I corrected this (and its brethren) and got an extra 2 compile-time errors where (here’s the odd part) I’ve never had a runtime error!?  Does this imply that code has never run?!  No.  But surely it should have failed in the same way!  Ahha, the runtime error indicates that the types allowed include interfaces … which is what I am supplying in the extra error case.  Hmm… how to specify a specific type OR any interface.  If memory serves, there was a way to say interfaces are fine.  In fact, it’s probably used in the runtime method I’m calling — GetComponent<T>().  Let’s decompile for a quick answer.  Oh, they don’t restrict.  Hmm…  Ah yes, use “class” for “class or interface” and “struct” for a “value types (like ints)”.  Ah, but how to combine?  Tried…

public static T GetComponentNonNull<T>(this Component comp) where T : class, Component {
}

…but no.  Hmm… so you can’t say both a type and interfaces (ahh, because generics is an “and” of all conditions — makes sense).  You can’t even do it with overloading (due to Erasure, I assume.  Er, I mean erasure).  Oh! C# doesn’t use erasure!  Well, learn something new every day!  Cool!  (Did I mention that I’m from a Java background?)  Hmm… still can’t contrive single-line solution.  Sigh, so had to solve with a hack: 2 methods, one with “Interface” stuck on the end whose type is constrained to “class”.  It’s close enough.  If anyone knows a better solution, I’d love to hear! (comment now and we’ll throw in a fine tweet free — not available in any stores!!)

 

Hmm… after adding those changes, I’m getting an assertion failure about there being no Rigidbody on my Snowman … which there isn’t until it rolls — but that’s not changed.  It would make sense if I had changed some initialization order thing but I haven’t.  Hmm… it’s the MoveWithPID which is a subclass of MoveToWithForceAndPID whose OnAwake() leads to the GetComponentNonNull<Rigidbody>() … which I have changed.  Could something here be acting differently due to the generics changes?  Seems unlikely.  All the same, I can’t disagree that MoveToWithForceAndPID shouldn’t be getting a Rigidbody in OnAwake().  For Snowman, which adds its Rigidbody when rolling starts, it needs doing at that time.  Viable since adding the OnRollStateChange event to SnowmanRoller just now.  Still odd though.  Perhaps better would be having the Rigidbody always present but switching it to Kinetic when the CharacterController is in use?  Would that work?  I’ll muse on this overnight — perhaps the morrow will make obvious what’s eluded me tonight.