#20 Better Camera & Better Players
Welcome to the twentieth devlog!
The past week or so I’ve been making improvements to the camera and player control again, and here I want to explain why and how I did that.
In an earlier devlog I explained creating a custom integrator for the player: basically, the player is a regular physics object (and obeys all the rules built into the game engine), BUT with some custom code that makes sure it doesn’t get stuck or fall over or something like that.
Well, so far this worked great, except … when colliding with stuff. Previously, I set the player velocity directly in the code:
set_linear_velocity( velocity_from_player_input )
Because of this, collisions had zero impact, because I “overruled” the collision results every frame with the player input. The result was a player character that could just kick packages (and even larger items) away as if they weighed nothing.
How do we fix this? Well, it’s quite simple really. We just ADD the input to our current velocity, instead of resetting the velocity each frame:
set_linear_velocity( get_linear_velocity() + velocity_from_player_input )
This brought some other problems with it. To make it work smoothly I also had to:
- Decrease the damping value. Otherwise, the player would stop moving TOO quickly, and it was very hard to get the player moving again once they had stopped.
- Set a maximum speed. Because I ADD the velocity, as long as I don’t bump into something, the velocity would just keep increasing and increasing. To fix this, I set a maximum velocity. (When calculating this velocity, the Y-axis is not taken into account, because gravity shouldn’t interfere with your movement speed. Falling down should just stay realistic.)
- Make most of the objects around the player heavier than the player itself. (Packages currently have a mass of 5, whilst the player has the default mass of 1. I also tested packages with a mass of 10, and although this gave everything a very nice weight, it made the game nearly unplayable, because boxes were just too hard to move.)
Now, collisions are registered and impact the player in a realistic way. It feels much nicer. It feels like the player actually has a proper weight to him.
Player maximum speed
After I created the new player code above … I quickly ran into problems.
I was testing a new mechanic with trampolines: jumping at angles. A trampoline always applies an impulse in its local upward direction. This means that if the trampoline is rotated to the side, it doesn’t just push the player upwards, it also pushes it to the side.
But … it didn’t work. Only upright trampolines worked.
It took me a while to realize that the problem wasn’t with the trampolines, but with the players. Because I set a maximum speed in the X and Z direction, any impulse applied by the trampoline was quickly reduced to almost nothing.
So I rewrote the system to something that I should have done the first time:
- The player has no default maximum speed. If you apply an impulse, the player will completely follow the laws of physics.
- However, when a player receives movement input, it performs an extra check. If the player is already moving faster than maximum speed, then the input is ignored. We don’t want the player to accelerate even more. (And as you might expect: if we’re below maximum speed, we’re allowed to accelerate, so listen to player input.)
Doing it this way, also meant I didn’t need to do anything with the Y-axis. The player simply obeys all the laws of physics – without me having to do anything – but only listens to player input when it’s below maximum speed.
Now everything works smoothly!
A player can only jump if it touches the ground.
That’s of course completely logical, but for the first few weeks of development, there were some problems with this. Sometimes, you were clearly standing on something, but you couldn’t jump!
Why did this happen? I use a “RayCast”, pointed towards the ground, to check if the player is standing on something. This ray starts from the center of the player and moves about 0.5 units downward. This means that if the player’s center isn’t touching the ground (which means the player is standing on the edge of something, or on a very strong slope), the code considers the player to be OFF the ground.
There are several ways to solve this.
- I could turn the player into a capsule, instead of a cylinder. A capsule has a rounded bottom (and top), which means that the center is the ONLY place where it can hit the ground. In this case, the RayCast will always be correct.
- I could use four raycasts, one for each corner. If at least one is hitting something, we’re on the ground. (Which object we’re standing on, or the slope of the ground, will be determined by majority vote: if three raycasts touch object A, and only one touches object B, we’ll assume we’re actually standing on object A.)
I’m currently testing these solutions to see which one creates the best gameplay.
While we’re on the topic of players, I want to explain something about the solo mode.
In solo mode, I instantiate TWO player characters, and allow the player to switch between them. (By pressing “S” on the keyboard, or the right shoulder button on controllers.)
When I first wrote the code, I thought this was going to be simple:
- Save a variable on each player that knows if this player is enabled or not.
- Input is ONLY registered on the enabled player.
- If input is the “switch button”, disable the current player and enable the other one!
Well … this didn’t work. When I pressed the button, the same player stayed active! Nothing happened! I thought I was going insane. It was only a few lines of code, and it really should have worked.
Then I realized that the switch was too fast.
Because player control switches within the same frame, the “switch button” is then ALSO registered on the other player, so it switched back! For me, this looked like nothing happened. But in reality it just switched back and forth between the players within a few milliseconds.
I know that I wanted some sort of animation when switching. Something that showed player control was being transferred. So I thought: why not use the delay from this animation to my advantage?
- I created a simple transparent sphere (which I call the “switch bulb” in my code).
- Whenever the switch button is pressed, the current player is IMMEDIATELY disabled.
- Then, this sphere starts moving from the old player to the new one.
- Once the sphere reaches the new player, THAT’s when the new player is enabled.
This creates a nice visual effect that clearly shows what’s happening, but also ensures there’s a few frames of delay between the switching. So far, this has never failed me.
As I’m refining the game idea and level design, the levels become smaller and smaller.
I also did some research on games with a similar (camera) style and found that you can actually zoom out quite a lot. If you set the right settings on the camera, it can be quite far way, and still show the level in a way that’s easy to interpret and play with.
First of all: I made the FOV (field of view) of the camera smaller. This means there’s less distortion around the edges AND gives it a more orthogonal/blocky view, which fits well with this game.
Then I moved the camera further backwards and allowed a greater zoom level, before splitscreen happens.
Splitscreen? Yeah, I forgot I implemented that as well. Looking at the current state of the game, and where it’s heading … we might not even need it! And any day a game does not need splitscreen is a good day.
(Seriously. It drains performance, kills loading times, and makes the screen much harder to interpret.)
I’m keeping the code/nodes used for splitscreen in my game, just in case, but I’ll try to make sure it’s never needed.
To finish this devlog, here’s a nice anecdote about how I can be very stupid at times.
Last night, I was working on a level with a tall building. (The cover image of this devlog shows the work in progress.) To make it look nice, I positioned the camera a little differently, so that players can see the building almost at a side view. (Instead of the regular view that is a mix between side view and top view.) But … the camera code was throwing all sorts of errors. After thirty minutes of frustration, I realized I had been very stupid. Remember how, in the devlog about the camera, I explained that the camera checks a vertical plane to see if it is still within level bounds on the Y-axis?
Yeah … I forgot to actually do that. I checked against `ground_plane`, instead of the variable `vertical_plane`. All the calculations were correct, all the information was ready to be used, I just forgot to change that variable where it mattered most. How this error slipped by me, I don’t know. I don’t even know if it’s a good thing that all the levels worked great with this bug. It might mean that the code is just very stable, it might also mean that the whole camera system is unnecessarily complicated :p
Get Package Party
Leave a comment
Log in with itch.io to leave a comment.