Revamping the looping mechanic to handle variable speed. I’m gonna break things!
I could record the position and timestamp. But then that won’t allow the clone to drift if the world impacts it, e.g., with a tractor beam, a singularity, or an actual impact. I guess this is effectively what I’m doing now.
I could record the velocity and timestamp. I’m leaning toward this right now. This will probably allow the clone to stick to its path fairly accurately when there’s no external force acting on it.
I could record the player inputs. When the turn buttons are pressed and released. This will mean there are far fewer events which is great for performance but I’d expect the clone to deviate from its original loop pretty quickly..? This option is the furthest from the current implementation.
The correct answer here might actually be the player inputs, though. Imagine I record the velocity at each timestamp. The clone runs into something and bounces away… or at least it should. Instead it just glitches out for one frame and then resumes its previous velocity (and probably runs into the thing again). That doesn’t work.
Wow, if the clone runs into something and doesn’t die it could end up in a loop that’s pointing in the opposite direction, potentially firing in that direction as well. That… sounds like a problem. Because of friendly fire that could be very bad for the player… a challenge maybe?
Anyway. I’m glad we had this chat. I’ll record player inputs instead of the velocity and timestamp. I wonder how much time I just saved myself by rubber ducking this.
…maybe that’s a good argument to implement the velocity and timestamp first. d-:
I’ll take a page from Claude’s book and write out my plan, too.
When Player gets the input events, it’ll instantiate PlayerEvent, just like when the gun is fired. CloneManager will pick up those events and store them, same as for the gun. PlayerTracker will still detect loop closures but it won’t need to include the list of positions in the signal. That will no longer be relevant.
Then when a CloneLoop is instantiated it gets the list of events. CloneManager already stores the gun fired events so it’ll store the movement events (and whatever else), too. So it can pass them to CloneLoop.
CloneLoop stores a list of points right now but instead it’ll only have the events. On each tick, it’ll replay any events that have come up. Right now the gun fired events are going straight to the projectile manager. That won’t do anymore. They need to be sent to the player instead. Don’t need to do events here, just a call to PlayerClone will do. The PlayerClone can figure out what to do with each event.
I haven’t componentized the code yet. So I’ll end up with duplicate code in Player and PlayerClone. Might be an idea to make player movement a component first.
Another part of this is that ProjectileManager will need to get its events from the Player and PlayerClone instances. It makes sense architecturally instead of getting them from the CloneManager is it does now. But that means I’ll need to manage the signal connections for the PlayerClone instances.
Are signal connects weak references?
Thanks to this StackOverflow answer it looks like a “yes.” https://gamedev.stackexchange.com/a/211668/196296
Or, more accurately, this comment says the destructor performs the signal disconnections. That probably means I can simply some of my other code that explicitly disconnects when an instance is removed. That’s great to know. I definitely lost some time on that during the jam.
Back to the plan. PlayerClone will be replaying a list of events. Player will also be generating them. If I use components… what do the components even look like?
Normally a PlayerMovement component would have a reference to the player and when controller input is received it changes the Player’s position. But if I want that code to be used by the PlayerClone I can’t get controller input, I’d want to get a PlayerEvent.
So have a PlayerInput component that just picks up controller input and generates PlayerEvents. Emits them as signals, reckon. PlayerMovement captures those events and moves the Player. In the clone there’s no PlayerInput… maybe a PlayerEventPlayer (lol that name).
I think I’ll do componentization later. Make sure this actually does what I want first. Then I’ll refactor.
So in short:
- CloneLoop to pass PlayerEvents to PlayerClone instances. That means ProjectileManager will receive GunFired events directly from the PlayerClone instances.
- Record PlayerEvents for movement keys.
- Change PlayerClone to use PlayerEvents for movement. Remove the Path2D. Training wheels are off now.
Should the CloneLoop or the PlayerClone store the events for replay? Maybe I can eliminate the CloneLoop entirely at this point. Before, its job was to instantiate the Path2D the PlayerClone would follow and to draw the art for the loop. Drawing the loop can be instantiated separately like explosions.
That’ll be nice. Having that extra layer between the PlayerClone and CloneManager has been a bit of a niggle.
Okay that was over an hour of just rubber ducking. A very useful hour but let’s do this.
…
Phew. Here’s the problem. Didn’t catch this in my planning earlier. The gun fired event needs to know the angle of the turret and that depends on the mouse position, or more succinctly, the angle from the player to the mouse. That value is gonna change basically every frame.
I could store it at the time that the gun is fired but that’d look silly if the clone’s turret is facing the wrong way right up until the moment it fires. I could update it every frame but that seems like overkill. I’d have to store a lot of events. I could tween toward the angle it should have the next time it fires. I could update it every, I dunno, every 50 milliseconds or so.
For now I’m gonna just update it at the time of firing. If it looks okay then great. If not then I’ll add it to the list of things to polish for later.
It’s kinda wild to me that I never even noticed the player clones didn’t have a turret which they were firing from. They were just spawning bullets from nowhere. Not sure if that means I can safely skip it or if it’s that’s my blind spot. I do have a tendency to miss details like these. Do my players?
…
Another thing I missed. I need to pass the player state to the clone. Well, more of it. I’m definitely passing some of it. But the clone needs to know if the mouse button was being held down before the loop started. Wow. That adds another layer. I guess it needs to know the last time the gun was fired as well so the cooldown can be respected?
That points toward a more player-oriented event tracking rather than player controller input tracking.
It’s a balance, though. If I track the moments that the gun fires then a cooldown buff / debuff that affects the clone and not the player won’t be able to have an effect.
I could just track whether the primary attack button was already being held down. I guess I’d have to do that for movement anyway. That will lead to an interesting problem later… or now, too, I suppose.
The player holds down fire and passes the loop start, then lets go before closing the loop. Now the clone starts off firing. Then stops firing. Then hits the end of the loop and spontaneously starts firing again. There won’t be an event for it. The player state will just change.
I’m so lucky I never got into all this during the game jam. Somehow just skimmed over it by using the Path2D and tracking only gun fire events.
So at the start of each loop should I just update the player state? And store in this player state things like whether it’s firing or not and … man, what if a loop closes at the point of a dash. Yeesh.
This is wild.
I think it’ll draw out some interesting mechanics so I’m partly frightened at getting this to work without some crazy bugs and excited about the possibilities.
For now I’ll just create a PlayerState which I can use to track whether the primary attack is being held down. PlayerInputState? When the clone is created and when it gets to end of its loop it will be reset to the PlayerState. But only certain things will get reset. And I guess I’ll only need to generate this at the moment the player is cloned… but I don’t get to know that moment until the loop is closed so I’ll have to keep a list of them. Haha. Alright. So be it.
I can’t decide whether to track whether the player is firing the gun or whether the primary attack key is being held down. Practically there is no difference, not for the gun. But it’ll definitely have an effect later. I’m ignoring the cooldown for now… and I think I’ll keep that. I’ll definitely have a more powerful weapon with a longer cooldown.
…
Thought about it a bit. It’ll be more fun for players if I restore the value of the gun cooldown at the start of the loop. That way they’ll be rewarded for carefully timing their loop. Get the loop to start just as the cooldown ends and you can get extra shots in. And like I said above, for powerful weapons with longer cooldowns this’ll make a big difference.
Using an area buff to decrease the cooldown is still viable. Instead of making the cooldowns shorter I just decrease them by a larger amount each tick. Golden.
So yeah. I won’t be able to just record whether the key is up or down in the PlayerInputEvent, I’ll need a separate PlayerState.