I just started learning game development in pygame and I want the player object to have a deceleration when the player stops pressing the key.
This is what I have at the moment:
def update(self):
self.accel_x = 0
keys = pg.key.get_pressed()
if keys[pg.K_LEFT]:
self.accel_x = -0.2
if keys[pg.K_RIGHT]:
self.accel_x = 0.2
if abs(self.vx) >= max_speed:
self.vx = self.vx/abs(self.vx) * max_speed
if self.accel_x == 0:
self.vx *= 0.91
self.vx += self.accel_x
self.vy += self.accel_y
self.rect.x += self.vx
self.rect.y += self.vy
It's works fine while moving to right but the object doesn't stop on time while going to left. Instead it decelerates to a point and then keeps going with a really slow speed for some time, then stops.
First, let see the math behind the algorithm.
When the button is pressed, the speed and position change based on the acceleration a, at t (number of times the function run), initial values being v0 and x0
v = v0 + a * t
x = x0 + Σ(i=1 to t) i * a
or
x = x0 + (t2+t) * a/2
And when the button is released (accel is 0) the speed v decreases geometrically
v = v0 * 0.91t
after 10 calls, we have ~0.39 v, after 100 calls ~10-5 v. Meaning that, visually, the position x decelerates and stops, v being too small to make a difference after some time.
The math is consistent with what is seen in games.
The question is why that algorithm doesn't work left side.
While it should work the same, left and right.
The difference is, left side,
speed v more likely to be negative after LEFT was pressed
position x might become negative at some point (and has to be checked)
Since the code provided (probably) does not cover the part to be changed, some recommendations:
You could force the speed to 0 if abs(v) is less than, say, 10-5 or another small values from which the position doesn't change visually (less than a pixel).
Ensure x values are checked at the limit, especially for negative values.
Debug: display/log v and x values especially after LEFT is released. This way when the whole program is running you'll identify more easily when does the problem come from.
If that doesn't address your problem, you could edit your question and add more relevant code.
Related
I have a flying car that I want to lose some HP when colliding with objects.
I connected car's RigidBody2D to this function to do that
func _on_Car_body_entered(body):
var force = linear_velocity.length()
var dmg = pow(force / 100, 2) - 0.25
if dmg <= 0: return
Health = Health - dmg
Now, since I don't have to be precise I'm just using current velocity as the force, though this is up for change.
After getting my 'force of impact', I put it into damage calculating formula and if damage is above 0, decrease HP by damage.
This works fine in most cases
BUT
I noticed that if car's going fast horizontally and just barely touch the ground (that's perfectly horizontal), car gets hit with a lot of damage, because I'm using the length of the velocity vector.
Ofcourse, this case can be managed by using just the Y component of the velocity vector, but then it removes any horizontal collisions, and vice versa, and it also leads me on to the path of programming vertical and horizontal collisions, and ofcourse those are not the only 2 directions of colisions I need.
Is there a way to remove the sliding factor from this equation?
You can get the sine of the angle between your velocity and the collision normal, and then take the absolute of that.
# 0 When sliding along the wall. 1 when hitting the wall head on
var slide_factor = abs(cos(vel_last_frame.angle_to(collision_normal)))
This will give you a value from 0 to 1. When you are just sliding along the wall, this value will be 0, and when you hit the wall straight on, it will be 1.
I am using the velocity from the last frame here so that it gets the velocity just before the collision. I get it by setting vel_last_frame to linear_velocity inside the _physics_process function.
You can only get the collision normal inside the _integrate_forces function using PhysicsDirectBodyState.get_local_contact_normal(), so you need to make a variable that can be accessed in this function and the _on_Car_body_entered function. Note that you need to set contact_monitor to true and contacts_reported to at least 1 for this function to work.
var collision_normal
func _integrate_forces(state):
# Check if there is a collision
if state.get_contact_count():
# contact_monitor must be true and contacts_reported must be at least 1 for this to work
collision_normal = state.get_contact_local_normal(0)
Now in the _on_Car_body_entered_function, you can multiply dmg by sliding_factor to scale it less depending on how much you are sliding against the wall.
func _on_Car_body_entered(body):
var force = linear_velocity.length()
# 0 When sliding along the wall. 1 when hitting the wall head on
var slide_factor = abs(cos(vel_last_frame.angle_to(collision_normal)))
var dmg = pow(force / 100, 2) - 0.25
# Reduce dmg depending on how much you are sliding against the wall
dmg *= slide_factor
if dmg <= 0: return
Health = Health - dmg
Found a solution for my problem here
This gives me a predictable force range to work with.
I copied all code for 2D collision, just added damage calculation
Range of forces my objects produce is <3000 for small collisions like scratches and bumps, ~10k for beginner friendly damage, and 20k+ for when I really slam the gas pedal, so I just convert that force to damage that I want.
Best part is that I don't have to use the body_entered from RigidBody2D, because now all my cars have this calculation in them, so when 2 of them collide they both get damaged.
extends RigidBody2D
var collision_force : Vector2 = Vector2.ZERO
var previous_linear_velocity : Vector2 = Vector2.ZERO
func _integrate_forces(state : Physics2DDirectBodyState)->void:
collision_force = Vector2.ZERO
if state.get_contact_count() > 0:
var dv : Vector2 = state.linear_velocity - previous_linear_velocity
collision_force = dv / (state.inverse_mass * state.step)
var dmg = collision_force.length() / 2000 - 2
if dmg > 0:
set_hp(Health - dmg)
emit_signal("Damaged")
previous_linear_velocity = state.linear_velocity
**OLD ANSWER**
RUBBER DUCK HERE
In script for car I added a new variable var last_linear_velocity = Vector2()
Then stored the last velocity in _process
func _process(delta):
last_linear_velocity = linear_velocity
Not in _integrate_forces because if I put it there then the new and last velocities are the same.
And just changed how force is calculated in the function mentioned above, so it looks like this
func _on_Car_body_entered(body):
var force = last_linear_velocity.length() - linear_velocity.length()
var dmg = pow(force / 100, 2) - 0.25
if dmg <= 0: return
Health = Health - dmg
Now I get a nice predicable range of values and can transform that to damage.
NOTE
I noticed that sometimes when collision occures the difference between the last and current velocity lengths is negative, as in - car is accelerating.
Anyway, this works for me for now.
If you find a better solutions do post it, as I couldn't find a solution to this problem online elswhere.
Below is my code (ignore the input variable it is just for movement):
var input = Vector3()
if Input.is_action_pressed("move_forward") and Input.is_action_pressed("move_left"):
rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(1, -1), delta * 7)
input[2] = 1
input[0] = 1
elif Input.is_action_pressed("move_forward") and Input.is_action_pressed("move_right"):
rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(1, 1), delta * 7)
input[2] = 1
input[0] = -1
elif Input.is_action_pressed("move_backward") and Input.is_action_pressed("move_left"):
rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(-1, -1), delta * 7)
input[2] = -1
input[0] = 1
elif Input.is_action_pressed("move_backward") and Input.is_action_pressed("move_right"):
rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(-1, 1), delta * 7)
input[2] = -1
input[0] = -1
else:
if Input.is_action_pressed("move_forward"):
rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(1, 0), delta * 7)
input[2] = 1
if Input.is_action_pressed("move_backward"):
rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(-1, 0), delta * 7)
input[2] = -1
if Input.is_action_pressed("move_left"):
rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(0, -1), delta * 7)
input[0] = 1
if Input.is_action_pressed("move_right"):
rotation_degrees.y = lerp_angle(rotation_degrees.y, atan2(0, 1), delta * 7)
input[0] = -1
For some reason, the player is hardly moving at all. Before I just set rotation_degrees.y to the direction but that didn't make a smooth movement. Any help is appreciated!
Getting Input
We have a better way to get a vector from input, let us start there:
var input := Vector3(
Input.get_action_strength("move_left") - Input.get_action_strength("move_right"),
0,
Input.get_action_strength("move_forward") - Input.get_action_strength("move_backward")
)
Addendum: This is the order you had. However, notice that we usually thing of x growing to the right, and - in Godot - forward is negative z. So this is backwards, I don't know if that is intentional.
What is going on here is that get_action_strength gives us the "strength" of the input, which is a number between 0 and 1. It will be 0 or 1 for a digital input, but it could be a value in between for analog input.
Then to get a component of the vector I take the input that would make it positive and subtract the negative. That gives you a value in the range from -1 to 1. Which also means we don't get a unit vector, depending on what you are doing, you might or might not want to normalize it.
Next you want the angle on the xz plane. Well, if you just used a Vector2, that would be trivial. So, instead of the code above, let us use a Vector2:
var input := Vector2(
Input.get_action_strength("move_left") - Input.get_action_strength("move_right"),
Input.get_action_strength("move_forward") - Input.get_action_strength("move_backward")
)
By the way, in Godot 3.4+ and Godot 4.0+ you will be able to use Input.get_vector, which simplifies this further.
It should not be hard to get a Vector3 from that if you need it for something else: Vector3(input.x, 0, input.y)
And now get the angle:
var input_angle := input.angle()
Addendum: You may need to do an operation on the angle to make it usable in 3D. The reason being that the angle is measured from the x axis, but no rotation is looking down the z. I had overlooked that when I wrote this answer. Although I tested all the code here. Without motion I didn't notice the orientation didn't match the direction of motion.
Smooth Rotation
Now, we want to smoothly change rotation_degrees.y to input_angle. There are multiple ways to do it, here are a few.
Before any of them. You are going to need to declare the target angle as a field, so the rotation does not reset when there is no input.
var _target_angle:float
And we are going to store it, whenever there is input:
var input := Vector2(
Input.get_action_strength("ui_left") - Input.get_action_strength("ui_right"),
Input.get_action_strength("ui_up") - Input.get_action_strength("ui_down")
)
if input.length_squared() > 0:
_target_angle = input.angle()
Note: I'm checking length_squared instead of length to avoid a square root.
lerp_angle
Yes, you should be able to use lerp_angle. Even thought, I don't advocate that.
I'll go ahead and add that 7 magic constant you have:
const _rotation_amount:float = 7.0
Then you would do it something like this (which I admit is convenient and short code):
rotation.y = lerp_angle(rotation.y, _target_angle, delta * _rotation_amount)
Please notice I changed rotation_degrees to rotation, that is because you get the angle in radians. Which would have been a problem using atan2 too. You can use deg2rad and rad2deg to do the conversions if you prefer.
With this approach, you have very little control over the interpolation. You do not specify how fast it rotates, nor how much time the rotation should take.
Please notice that the weight of a linear interpolation is not time. You are not telling Godot "go from this value to this value in this amount of time" (you can do that with a Tween, see below).
Also notice that it will advance a more or less stable proportion of the difference of the values each frame. Thus it moves more at the start, and less at the end. That is, this is not linear at all. It has a deceleration effect.
Angular speed
If we are going to work with angular speed, it follows that we need a field to hold the angular speed:
const _angular_speed:float = TAU
Here TAU (which is PI * 2) represent one rotation per second (four rotations per second would be 4 * TAU, and a quarter rotation per second could be 0.25 * TAU, you can associate TAU with one Turn as mnemonic).
Now, we are going to figure out the angle difference (shortest path), to do that we can do this:
var angle_diff := wrapf(_target_angle - rotation.y, -PI, PI)
The wrapf function will "wrap" the value in the range (-PI to PI in this case), so that if the values goes beyond that range, it is as if it entered from the opposite edge (if you say wrapf(14, 0, 10) you get 4, and if you say wrapf(-4, 0, 10) you get 6). I hope that makes sense.
To apply the angular speed, in theory, we would only need the sign:
rotation.y += delta * _angular_speed * sign(angle_diff)
However, we don't want to overshoot. We can solve this with clamp:
rotation.y += clamp(delta * _angular_speed, 0, abs(angle_diff)) * sign(angle_diff)
Notice I have separated the sign from the magnitude of angle_diff. I'm clamping how much the angle would change according to angular speed to the magnitude of angle_diff (so it is never more than angle_diff, i.e. it does not overshoot). Once clamped, I apply the sign so it turns in the correct direction.
And, of course, if you really wanted to, you could work with angular acceleration and deceleration too.
Tween
Using a Tween node is the most powerful option sans using an AnimationPlayer. And it is also very easy to use.
We, of course, need to define a Tween object and add it as a child:
var _tween:Tween
func _ready() -> void:
_tween = Tween.new()
add_child(_tween)
This is because the Tween object will continue to gradually change the rotation each frame (so we don't have to worry about it). And to do that, it needs to persist and to get frame notifications, so it needs to be in the scene tree somewhere.
In Godot 4.0+, you will be able to do get_tree().create_tween() which returns a rooted Tween, so you don't have to worry about keeping a reference or adding it to the scene tree.
And then you can do this:
var input := Vector2(
Input.get_action_strength("ui_left") - Input.get_action_strength("ui_right"),
Input.get_action_strength("ui_up") - Input.get_action_strength("ui_down")
)
if input.length_squared() > 0:
var rotation_time := 0.2
_tween.interpolate_property(self, "rotation:y", rotation.y, input.angle(), rotation_time)
_tween.start()
Do not forget to call start. Well, In Godot 4.0+, all tween will automatically start by default.
As you can see, the code is telling the Tween to interpolate the y of the rotation from the value of rotation.y (its current value) to the value of input.angle(), and we specify a rotation time (which I made a variable just so I can give it a name). And that's it.
In Godot 4.0+, interpolate_property is gone in favor of the new tween_property API, with which you don't need to specify the start value.
Oh, wait, we can specify how do you want to ease these values:
_tween.interpolate_property(
self,
"rotation:y",
rotation.y,
input.angle(),
rotation_time,
Tween.TRANS_QUAD,
Tween.EASE_OUT
)
See this cheatsheet by wandomPewlin to pick your easing:
I believe Tween.TRANS_QUAD and Tween.EASE_OUT gives you something close to what you get from lerp_angle, except stable. And look, you have more options!
I'll also mention that you can ask Tween what interpolation it currently is doing, and also remove them, which is useful in some cases. Yet, you don't need to go into that for simple cases like this one. And, well, there is AnimationPlayer if you need something more complex. By the way, yes, AnimationPlayer can operate on a subproperty (such as rotation.y, I have an explanation elsewhere).
I am trying to code a boat that keeps its momentum after letting go of "w". However, i also want it to slow down gradually after letting go of "w". I have gotten a decent boat working but sometimes when I press "w" after going a certain direction, the boat goes flying. Is there any way to add a top speed or make it slow down?
self.thrust = 0.0
self.acceleration = 0.0001
self.dx += math.cos(math.radians(self.heading)) * self.thrust
self.dy += math.sin(math.radians(self.heading)) * self.thrust
def accelerate(self):
self.thrust += self.acceleration
def decelerate(self):
self.thrust = 0
If you want it to slow down gradually when you aren't pressing "w", then you may want it to go at the same speed, or faster. This code will work for it to go at the same speed. This should work in any Python IDE.
Suppose your boat is a turtle.
import turtle
Win = turtle.screen()
Boat = turtle.Turtle()
If you want it to also look like a Boat, you can do:
screen.adshape(Saved Document Direction)
Boat.shape(Saved Document Direction)
Otherwise, you can create your own shapes.
Now, we want it to go at a constant speed, while you press "w":
So:
def Constant_Speed():
Boat.speed(Speed You Want it to Go)
Win.listen()
Win.onkeypress(Constant_Speed, "w")
Otherwise, it should slow down.
Initial_Speed = Initial Speed
Boat.speed(Initial_Speed)
Initial_Speed *= Fraction of what you want it to be in/decimal
So, this should work if you replace with your variables:
import turtle
Win = turtle.screen()
Boat = turtle.Turtle()
screen.adshape(Saved Document Direction)
Boat.shape(Saved Document Direction)
def Constant_Speed():
Boat.speed(Speed You Want it to Go)
Win.listen()
Win.onkeypress(Constant_Speed, "w")
Initial_Speed = Initial Speed
Boat.speed(Initial_Speed)
Initial_Speed *= Fraction of what you want it to be in/decimal and must be less than 1
But what if you want it to go faster, not at a constant speed?
Then go to the def Constant_Speed() part.
You can rename it: def Go_Faster()
Now, the code:
Speed = Initial_Speed
Boat.speed(Speed)
Speed *= Times you want to increase speed by
Also:
Change the last line to:
Initial_Speed2 = Initial Speed * Fraction of what you want it to be in/decimal and must be less than 1
With your variables.
Now, this should work:
import turtle
Win = turtle.screen()
Boat = turtle.Turtle()
screen.adshape(Saved Document Direction)
Boat.shape(Saved Document Direction)
def Go_Faster():
Speed = Initial_Speed()
Boat.speed(Speed)
Speed *= Times you want to increase by
Win.listen()
Win.onkeypress(Constant_Speed, "w")
Initial_Speed = Initial Speed
Boat.speed(Initial_Speed)
Initial_Speed2 = Initial Speed * Fraction of what you want it to be in/decimal and must be less than 1
This is my first time posting a question.
I'm having trouble creating a code involving cosine, and I am not recieving the desired outcome. What is even more confusing is the fact that the two codes should be creating similar images (Explained later). Any ideas?
In the code below, these variables represent:
Y is a counter, making sure that the code only runs until the specified amount of radi is produced.
W is the colour randomly generated.
Z is the angle turn from 0 degrees. (The turtle's angle resets due to turtle.home).
Adjacent is the smallest length from centre to a line.
Radi is the amount of lines protruding from the centre.
def Triangle(Radi, Adjacent):
y = 0
if (Radi) % 1 == 0:
while (Radi) > y:
y = y + 1
w = randhex()
z = 360/(Radi)*y
turtle.left(z+30)
turtle.color(w)
if z > 300:
turtle.forward(Adjacent/math.cos(math.pi*(60 - (z - 300))/180))
elif z > 240:
turtle.forward(Adjacent/math.cos(math.pi*(z - 240)/180))
elif z > 180:
turtle.forward(Adjacent/math.cos(math.pi*(60 - (z - 180))/180))
elif z > 120:
turtle.forward(Adjacent/math.cos(math.pi*(z - 120)/180))
elif z > 60:
turtle.forward(Adjacent/math.cos(math.pi*(60 - (z - 60))/180))
else:
turtle.forward(Adjacent/math.cos(math.pi*z/180))
turtle.home()
Above is my first code which appears to work, giving these results when Triangle(100,180) is entered (Please note that randhex() is a custom function that generates random colours).
Triangle(100,180) results.
My apologies if my variable naming creativity is annoying.
In this code, counter represents 'y' and angle represents 'z' from the previous code
Here is my second code:
def Polygon(Radi, Adjacent, Sides):
counter = 0
if Sides % 1 != 0 or Sides == 2 or Sides <= 0:
print ("INVALID")
elif Sides == 1:
while Radi > counter:
counter = counter + 1
colour = randhex()
turn = 360/Radi*counter
turtle.left(turn)
turtle.color(colour)
turtle.forward(Adjacent)
turtle.home()
else:
while Radi > counter:
counter = counter + 1
colour = randhex()
turn = 360/Radi*counter
turtle.left(turn)
turtle.color(colour)
segment = str(counter/Radi*Sides*2)
position = segment.index('.')
test = int(segment[:position])
if test % 2 == 1:
length = Adjacent/math.cos(math.pi*(turn - (360 - 360/Sides*((test+1)/2)))/180)
turtle.forward(length)
else:
length = Adjacent/math.cos(math.pi*(180/Sides - (turn - (360 - 180/Sides*(test+1))))/180)
turtle.forward(length)
turtle.home()
Above is my second code, being the one I'm struggling with. Once again, apologies for my variable names being annoying and some of the maths not simplified. I find it easier to see how my ideas make sense when I leave them as they are. Below are my results for my second code after entering Polygon(180,100,3).
Polygon(180,100,3) results.
As you can see, it didn't go quite how I was planning.
I should also note that I tried substituting the numbers into the codes where one of the codes were giving a different line length. Sometimes they even went in an opposite direction (because the number came out negative). I did this on the Google calculator, but it seemed that both codes would give the same answer, but they corresponded to what the second code was outputing, not the first.
If you want me to explain anything leave a comment.
But if it turns out that my code is wrong (Which I believe), could you please point me to what I need to do instead.
I'd appreciate the help.
Your code is too complicated to debug. The unhelpful variable names, the lack of comments and excessively long equations make it hard to read.
If we consider the equation suggested in this answer to Is there an equation to describe regular polygons? then your original triangle code simplifies to:
import math
import turtle
def randhex():
""" substitute random color generator here """
return 'red'
def triangle(radii, adjacent):
if radii % 1 != 0: # only whole numbers (int or float) allowed
return
counter = 1
while counter <= radii:
angle = counter * (2 * math.pi / radii)
turtle.setheading(angle)
colour = randhex()
turtle.color(colour)
radius = adjacent / math.cos(angle % (math.pi / 1.5) - math.pi / 3)
turtle.forward(radius)
turtle.backward(radius)
counter += 1
turtle.radians() # avoid individual conversions, switch to radians
triangle(100, 180)
turtle.exitonclick()
And the general polygon solution can be achieved with just a few changes:
import math
import turtle
def randhex():
""" substitute random color generator here """
return 'red'
def polygon(radii, adjacent, sides):
if radii % 1 != 0: # only whole numbers (int or float) allowed
return
if sides % 1 != 0 or sides == 2 or sides <= 0:
return
counter = 1
while counter <= radii:
angle = counter * (2 * math.pi / radii)
turtle.setheading(angle)
colour = randhex()
turtle.color(colour)
if sides == 1: # special case, a circle
radius = adjacent
else:
radius = adjacent / math.cos(angle % (math.pi / (sides / 2)) - math.pi / sides)
turtle.forward(radius)
turtle.backward(radius)
counter += 1
turtle.radians() # avoid individual conversions, switch to radians
polygon(100, 180, 3)
turtle.exitonclick()
With polygon(100, 90, 5) looking like:
I've made a simple terrain generator, but it takes an excessive amount of time to generate anything bigger than 50x50. Is there anything I can do to optimise the code so that I can generate larger things? I know that things such as pygame or numpy might be better for doing this, but at my school they wont install those, so this is what I have to work with.
Here's the relevant code:
def InitMap(self):
aliveCells = []
for x in range(self.width):
for y in range(self.height):
if random.random() < self.aliveChance:
aliveCells.append(self.FindInGrid(x,y))
return aliveCells
def GenerateMap(self):
aliveCells = self.InitMap()
shallowCells=[]
self.count = 1
for i in range(self.steps):
aliveCells = self.DoGenStep(aliveCells)
for i in aliveCells:
self.canvas.itemconfig(i,fill="green")
for i in aliveCells:
for j in self.FindNeighbours(i):
if j not in aliveCells: self.canvas.itemconfig(i,fill="#0000FF")
def DoGenStep(self,oldAliveCells):
newAliveCells = []
for allCells in self.pos:
for cell in allCells:
self.root.title(str(round((self.count/(self.height*self.width)*100)/self.steps))+"%")
self.count += 1
aliveNeighbours = 0
for i in self.FindNeighbours(cell):
if i in oldAliveCells: aliveNeighbours += 1
if cell in oldAliveCells:
if aliveNeighbours < self.deathLimit:
pass
else:
newAliveCells.append(cell)
else:
if aliveNeighbours > self.birthLimit:
newAliveCells.append(cell)
return newAliveCells
def FindNeighbours(self,cell):
cellCoords = self.GetCoords(cell)
neighbours = []
for xMod in [-1,0,1]:
x = xMod+cellCoords[0]
for yMod in [-1,0,1]:
y = yMod+cellCoords[1]
if x < 0 or x >= self.width: pass
elif y < 0 or y >= self.height: pass
elif xMod == 0 and yMod == 0: pass
else: neighbours.append(self.FindInGrid(x,y))
return neighbours
NB: You didn't add the method "FindInGrid", so I'm making some assumptions. Please correct me if I'm wrong.
One thing which would help immensely for larger maps, and also when at high densities, is not to store just the alive cells, but the entire grid. By storing the alive cells, you make the behavior of your program in the order O( (x*y)^2), since you have to iterate over all alive cells for each alive cell. If you would store the entire grid, this would not be necessary, and the calculation can be performed with a time complexity linear to the surface of your grid, rather than a quadratic one.
Additional point:
self.root.title(str(round((self.count/(self.height*self.width)*100)/self.steps))+"%")
That's a string operation, which makes it relatively expensive. Are you sure you need to do this after each and every update of a single cell?