Alex Mason

Softwarrister, friend to animals, beloved by all

Dev log #4 – Nascent stem of cracking Rocks

Last time, I hit a wall, literally with the shots, making them bounce, but also metaphorically because I couldn’t solve the problem where I had raycasts hitting colliders from the inside and scheduling incorrect bounces. I’ve thought about this while walking my dogs several times, considering how I might cast multiple rays to project the path in advance, keep track of the IDs of the bounce surfaces so that I discard a RaycastHit if it shows up twice in a row, hold the shot’s collider ahead of its path, et cetera.

There were enough possibilities that I decided to web search engine it, because this type of game has been made loads of times, and other people must have solved this problem. Eventually I ran into this video, Creating an Infinite Bouncing Ball with Physic Materials (Unity Tutorial)

I didn’t even watch it, I just looked at the first few seconds and thought, “Okay, this is 3D but maybe it’ll apply to 2D, so the balls have gravity which means they’re controlled by the physics engine but they’re also not losing steam, per the title. Oh, so that’s doable with a physics material!” Per usual, I was overthinking it this whole time.

So I created a new material, saw the word Shader in its Inspector panel and realized I needed a physics material. So I created a physics material and set the Bounce to 1 (maximum value). Then I tried to add it to the capsule’s Rigidbody2D and I couldn’t! Of course, things like physics material are implicitly 3D without a suffix. So I created a 2D physics material under the 2D section of the context menu in the assets explorer, and set the Bounce to 1 again, and Friction to 0. That went on to both the shot and the capsule, and the shot became dynamic instead of kinematic again, with zero gravity. And it just worked! The shots slowed down during testing though, and I realized Angular drag was the culprit (I thought it only affected rotation), so I set that to 0. But what’s this?

With the GIF’s frame rate it looks more like snow, but if you look at the top, you’ll see some of the shots got stuck against the cieling, which is quite peculiar. It seems to happen when they hit at a narrow enough angle. Why would physics do this? I now realize I haven’t given the new material to the walls, so it must be caused by the default Friction value. So I put the new material on the walls, and it turns out I can also set it as the Default material in Project Settings.

I even learned from the Collision Action Matrix that I could remove the static Rigidbody2Ds from the walls, because their colliders will still apply the physics to the dynamic (but not kinematic) Rigidbody2D shots. In either case, the wall hugging persists. Maybe this is one of many reasons shots should decay after some number of bounces that don’t hit destructibles.

But I still want a definitive fix, so I used the search query [rigidbody2d slides on wall instead of bouncing off], and found this solution, which involves changing a Physics setting. Now if we look at the manual for Physics, we see:

Note: To manage global settings for 2D physics, use the Physics 2D settings instead.

It said Instead, so the Physics settings won’t affect my objects. I was already messing with the 2D Physics settings last time, and I didn’t see anything about Bounce. However, there is a Velocity Threshold:

Set the threshold for elastic collisions. Unity treats collisions with a relative velocity lower than this value as inelastic collisions (that is, the colliding GameObjects do not bounce off each other).

The default Velocity Threshold is 1. The shots have a speed that I set to 8, but the Relative velocity must be the veloctiy as projected onto the axis of collision. I’d like to set the threshold to 0, but the minimum is 0.0001. Should be fine.

And it works!

Now I can give the shooting system a beginning and end. On the “fire” input, I want to:

  • remove the aim line
  • release some number of shots in sequence
  • despawn shots when they return to the shooter’s region (or go too long without dealing damage)
  • detect when all shots have “Returned”

Removing the aim line is dead easy, starting by defining a bool named isAiming.

    bool isAiming = true;
    ...
    void DisableAim() {
        isAiming = false;
        aimLine.enabled = false;
    }

    void Update() {
        if (isAiming) {
            ShowAimLine();
        }
        if (Input.GetMouseButtonDown(0) && isAiming) {
            DisableAim();
            Shoot();
        }
    }

Until I establish the way to detect the end of the shot, I will only get one shot per play while testing.

Now to fire many shots in sequence, I believe there’s a function I can use to delay calling another function. It’s called Invoke, and it works in my case because my function doesn’t take parameters. Now I need a public field for the time between individual shots, and the number thereof. I change Shoot() to ScheduleShots() in Update.

    void ScheduleShots() {
        for (int i = 0; i < numShots; ++i) {
            Invoke(nameof(Shoot), i * shotInterval); 
        }
    }

Let’s see what an interval of 0.2 looks like. Oh, and I just found out I can embed Loom videos here. I didn’t see a way to crop the recording, but maybe seeing the scene hierarchy is worth the tiny player.

NNow it’s time to despawn shots. I’ll start with a trigger-only collider in the shooter’s bay, and let shots despawn when they enter. But if the shots touch that collider when they spawn, that might just cause them to despawn instantly. Therefore, I should add a condition that gets set on exit from the collider, which will then permit them to despawn next time.

I can add the collider as a component of the shooter for now, but I will want to make it a child of a general manager for that area, so that the shooter can change positions without worrying about the position of the collider. When I start refactoring I’ll call the blog post “Falling leaves” or somthing.

So for the shot script I added a Retire() function even though it’s just wrapping one line, because I plan to add a little animation or make it go back to the shooter and be “absorbed” in some way.

   bool canRetire = false;
   ...
   void Retire() {
        gameObject.Destroy();
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (canRetire) {
            Retire();
        }
    }
    
    void OnTriggerExit2D(Collider2D other)
    {
        canRetire = true;
    }

Does it work? Yes, but the aim line is now truncated because its raycast hits the new collider. Thus I will put it in a layer that the aim line can ignore. And it turns out there’s already an Ignore Raycast layer built in, and the raycast does ignore it! So here’s the shots “retiring,” and having a nice little dance on the way back up. It looked better in 60FPS.

Now to count the empty bounces before retirement. Variable declarations omitted.

    void AddEmptyBounce() {
        numEmptyBounces += 1;
        if (numEmptyBounces > emptyBounceThreshold) {
            Retire();
        } 
    }
    void Hit(Destructible destructible) {
        numEmptyBounces = 0;
        numHits += 1;
        bool destroyed = destructible.TakeDamage();
        if (destroyed) {
            numDestroyed += 1;
            Debug.Log("Destroyed a block!");
        }
    }
    void OnCollisionEnter2D(Collision2D other)
    {
        Destructible destructible = other.gameObject.GetComponent<Destructible>();
        if (destructible == null) {
            AddEmptyBounce();
            return;
        }
        Hit(destructible);
    }

Now how will I know when the shots are all retired? Well, the Retire() function can just tell The shooter, which keeps a count of retirement events. The shooter might not be the only thing that will want to know about a shot’s retirement, so I can make it an event, using Destructible‘s HealthChanged event as a template. I’m not going to show a bunch of code snippets this time. Use your imagination.

Wow, an actual discernible gameplay loop!

Next time, I’ll investigate trails, more types of blocks, and moving the blocks up each turn.

Cover art: NASA/Bill Ingalls, Perseid Meteor shower, August 2015