I have a question .
I'm making a platformer game in tkinter and I have an issue :
I have for now : player , blocks and coins .
I'm updating the player's move and it's animation and the coin's animation and for some reason when I'm putting too much coins , the player's movement starts lagging.
Note: I'm using the after function of tkinter for animations for player's movement + animation and same goes for the coins .
For other things like gravity and etc I'm using just threads .
code of coins updating :
def coinsCheckCollision(self):
cRemove = None
indexRemove = -1
count = 0
for c in self.frame.coins:
x, y , width , height = c.getRectangle()
xP = self.player.getX; yP = self.player.getY; wP = self.player.getWidth; hP = self.player.getHeight
if collisionDetect(xP , x, yP , y, wP , width, hP , height) or collisionDetect(x , xP , y , yP , width , wP , height , hP):
if count not in coinsRemoved:
indexRemove = count
if indexRemove != -1:
if indexRemove not in coinsRemoved:
coinsRemoved.append(indexRemove)
count +=1
def coinsUpdateAnimations(self):
count = 0
for c in self.frame.coins:
if count not in coinsRemoved:
self.img = c.getAnimation()
self.img = ImageTk.PhotoImage(self.img)
self.frame.coinsImages[count] = self.img
else:
if self.frame.coinsImages[count] is not '' :
self.frame.coinsImages[count] = ''
self.frame.canvas.delete('coinB'+str(count))
what = self.frame.canvas.itemconfig('coin' + str(count), image=self.frame.coinsImages[count])
count += 1
self.coinsCheckCollision()
self.frame.frame.after(40 , self.coinsUpdateAnimations)
Anyway , the question in short is : why when I'm updating multiple things that aren't really "related" to each other , the gui starts lagging ?
Your design seems to expect your functions to run every 40ms. Maybe +/- a few ms, but averaging 25 times per second.
But that's not what happens.
First, how many coins do you have, and how complicated is that collisionDetect function? If it only takes a tiny fraction of 1ms to run through that loop, it's no big deal, but think about what happens if it takes, say, 15ms: You wait 40ms, then do 15ms of work, then wait another 40ms, then do 15ms of work, etc. So your work is running only 15 times per second, instead of 25.
Now imagine each coin takes, say, 0.2ms. At 3 coins, there's a lag of 0.6ms, which is barely noticeably. But at 100 coins, there's a lag of 20ms. That slows the coins down by 50%, which is pretty obviously noticeable.
Second, as the docs say:
Tkinter only guarantees that the callback will not be called earlier than that; if the system is busy, the actual delay may be much longer.
Being off a few ms randomly in either direction might be fine; it would all average out in the end. But after is always a few ms late, never a few ms early, so instead of averaging out, it just builds up and you get further and further behind.
And, worse, if one of your functions gets behind, it will tend to make the delay in each after a bit longer—so it won't just be your coin animation slowing down 50%, but the whole game slowing down by some unpredictable amount arbitrarily between 0-50%, but probably enough to be noticeable.
To solve both problems, you need to carry around something like the time you expected to run at, then, instead of doing after(40), you do something like this:
expected_time += 40
delay = expected_time - current_time
after(max(0, delay), func)
To put it in concrete (although untested) terms, using the datetime module:
def __init__(self):
self.next_frame_time = datetime.datetime.now()
self.schedule()
def schedule(self):
self.next_frame_time += datetime.timedelta(seconds=0.040)
now = datetime.datetime.now()
delta = max(datetime.timedelta(), now - self.next_frame_time)
self.frame.frame.after(delta.total_seconds * 1000, self.coinsUpdateAnimations)
def coinsUpdateAnimations(self):
# all the existing code before the last two lines
self.coinsCheckCollision()
self.schedule()
This still won't solve things if the total work you do takes more than 40ms, of course. Imagine that you spend 50ms, then do an after(0, func), which triggers at least 10ms late, and then spend another 50ms, then the next after(0, func) triggers at least 20ms late, and so on. If you can't do all of your work in something that's usually significantly less than 40ms, you won't be able to keep up. You have to either:
Find a way to optimize your code (e.g., maybe you can use a better algorithm, or use numpy instead of a for loop),
Redesign your game to do less work, or
Slow down your frame rate to something you actually can keep up with.
A possibly better solution is to stop trying to bend Tkinter into a gaming framework. It's not designed for that, doesn't help you get all the fiddly details right, and doesn't work all that well even once you do get them right.
By contrast, something like Pygame Zero is, as the name implies, designed for creating games. And designed to make it easy enough that people with a lot less Python experience than you seem to have can use it.
For example, instead of an event loop that runs at whatever speed your OS wants to run it, making it your responsibility to get everything timed right, Pygame Zero runs a frame loop that calls your update function N times per second, as close to evenly as possible. And it has built-in functions for things like collision detection, drawing animated sprites, etc.
Related
I need to create various rate timers of events across a timed period. For example, 5 events/16 seconds, or real-time. Akin to bandwidth calculations.
From what I can tell, this is something that would need to be written from scratch, as the timing functions I've seen are for performing an event every X seconds.
Questions:
Are there libraries for this type of thing, or would they need to be written from scratch?
I have a fair stab at some manual functions, for example for events over time_len:
def time( self,quantity ):
self.this_ts = c.time.time_ns()
self.this_diff_ts = self.this_ts - self.last_ts
if( self.this_diff_ts < self.time_len ):
self.this_count += quantity
else:
self.this_rate = self.this_count
self.this_count = quantity
self.ticks = 0
self.last_ts = self.this_ts
Is that a reasonable approach?
How is a real-time rate actually calculated? Would it be a count of events within a second averaged over seconds?
Thank you,
John
I am using the psychopy code from this publication: "Using a variant of the optomotor response as a visual defect detection assay in zebrafish". The code is a series of while loops on a countdown. Each loop should run for 10 seconds. I want to add another countdown before the first 10 seconds loop that will last for 5 seconds and will just be a black screen.
The problem that I am having is that the first 5 seconds while loop (the black screen) is overlapping with the first 10 seconds while loop. This results in the first 10 second loop only lasting for 5 seconds. Oddly enough, the rest of the loops that are supposed to last for 10 seconds each are not overlapped or disturbed.
Does anyone have any ideas why this might be happening? I don't get any errors when I run my code.
Irrelevant note: I am planning on changing the times for each loop
#import libraries needed from PsychoPy
from psychopy import visual, core, event, sys
#create a window
mywin = visual.Window([1680,1050], monitor="testMonitor", units="deg", fullscr = True)
#To run the stimulus, "save" + "run"
#Create the black screen for acclimation
TIMEBLACK = 5
black = visual.GratingStim(win=mywin, size=999, pos=[0,0], sf=0, color=[-1,-1,-1])
blacktimer = core.CountdownTimer(TIMEBLACK)
#Create OMR Stimuli
TIME = 10 #this is seconds of stimulus display and should be changed based on experimental needs (See Table 2)
SPEED = 1.04 #1.033 is adult optimal, 1.04 is larvae optimal. See table for additional speeds; correlates to 12 angular cycles for adults and 16 angular cycles for larvae.
grating = visual.RadialStim(win=mywin, mask='circle', tex='saw',size=20, color=[1,1,1],pos=[0,0],angularCycles = 4, angularRes = 3600, contrast = -1.0) #angularCycles are the number of black/white bars presented by the stimulus. Adult optimal is 12 angular cycles, larvae optimal is 16 angular cycles (See Table 1).
fixation = visual.GratingStim(win=mywin, size=999, pos=[0,0], sf=0, color=[0.5,0.5,0.5])
timer = core.CountdownTimer(TIME)
while blacktimer.getTime()>0:
black.draw()
mywin.flip()
if len(event.getKeys())>0: sys.exit(0)
event.clearEvents()
blacktimer.reset(TIMEBLACK)
#draw the stimuli and update the window. The phase is always advanced by 0.05 of a cycle.
while True: #this creates a never-ending loop
while timer.getTime()>0:
grating.setAngularPhase(SPEED, '-')
grating.draw()
mywin.flip()
if len(event.getKeys())>0: sys.exit(0)
event.clearEvents()
timer.reset(TIME)
while timer.getTime()>0:
fixation.draw()
mywin.flip()
if len(event.getKeys())>0: sys.exit(0)
event.clearEvents()
timer.reset(TIME)
while timer.getTime()>0:
grating.setAngularPhase(SPEED, '+')
grating.draw()
mywin.flip()
if len(event.getKeys())>0: sys.exit(0)
event.clearEvents()
timer.reset(TIME)
while timer.getTime()>0:
fixation.draw()
mywin.flip()
if len(event.getKeys())>0: sys.exit(0)
event.clearEvents()
timer.reset(TIME)
if len(event.getKeys())>0: break
event.clearEvents()
#cleanup. To exit, press any key.
mywin.close()
core.quit()
Here are my updates after edits. I moved everything having to do with the black screen to be before the first 10 second while loop:
#Create the black screen for acclimation
TIMEBLACK = 5
black = visual.GratingStim(win=mywin, size=999, pos=[0,0], sf=0, color=[1,-1,-1])
blacktimer = core.CountdownTimer(TIMEBLACK)
while blacktimer.getTime()>0:
black.draw()
mywin.flip()
if len(event.getKeys())>0: core.quit(0)
blacktimer.reset(TIMEBLACK)
#Create OMR Stimuli
TIME = 10 #this is seconds of stimulus display and should be changed based on experimental needs (See Table 2)
SPEED = 1.04 #1.033 is adult optimal, 1.04 is larvae optimal. See table for additional speeds; correlates to 12 angular cycles for adults and 16 angular cycles for larvae.
grating = visual.RadialStim(win=mywin, mask='circle', tex='saw',size=20, color=[1,1,1],pos=[0,0],angularCycles = 4, angularRes = 3600, contrast = -1.0) #angularCycles are the number of black/white bars presented by the stimulus. Adult optimal is 12 angular cycles, larvae optimal is 16 angular cycles (See Table 1).
fixation = visual.GratingStim(win=mywin, size=999, pos=[0,0], sf=0, color=[0.5,0.5,0.5])
timer = core.CountdownTimer(TIME)
#draw the stimuli and update the window. The phase is always advanced by 0.05 of a cycle.
while True: #this creates a never-ending loop
while timer.getTime()>0:
grating.setAngularPhase(SPEED, '-')
grating.draw()
mywin.flip()
if len(event.getKeys())>0: core.quit(0)
timer.reset(TIME)
In the original version of your code, you create the first timer like this:
blacktimer = core.CountdownTimer(TIMEBLACK) # will run for 5 s
You then create a second one like this:
timer = core.CountdownTimer(TIME) # will run for 10 s
Each timer begins counting down from the moment it is created (or reset). So the second one (timer) will have counted down to ~ 5 seconds by the time the first (blacktimer) has reached 0 s, meaning that by the time you start using timer, half of its time has already elapsed.
Each subsequent time that you use timer, it will run correctly for 10 s, as you are re-setting it immediately before each loop.
Your revised version of the code seems to address this issue.
Additional suggestions: you should avoid calling sys.exit(0). PsychoPy provides a more graceful way of exiting via core.quit(), which does some tidying up on the way out, including saving any current data. Also no need to keep calling event.clearEvents() here: if a key event had occurred, your code would have exited.
Lastly, the recommendation these days (even for experienced Python developers) is to use the Builder GUI to generate the script for you. It uses best-practice techniques to give you good timing, and uses up-to-date APIs. For example, Builder scripts no longer use the old event module for keypress handling, instead using the much higher-performance Keyboard object. The GUI still allows you to insert custom Python via its graphical "Code Components", for when you need functionality that the GUI doesn't provide. It takes care of ensuring the code runs at the right time (e.g. at the beginning of each trial, or on every screen refresh) and means you can focus on whatever your unique functionality is, while all the housekeeping of recording data and showing stimuli on time is taken care of for you.
You'll also find that the best support for PsychoPy is from the dedicated forum at https://discourse.psychopy.org, rather than here at StackOverflow. The PsychoPy developers themselves are much more likely to see your posts there.
I'm using a game engine to make my own 3D game in python. I need to simulate a jump and/or gravity, but I'm running into an issue : either the calcul is immediate and my character isn't moving, like, there's no animation, instant blink, or either (if for exemple I had a bigger number in my for loop) it takes wayyyy more time, really slow, it lags and everything, struggling to calculate the jump. I got both extremes, and none works, so I'd like to find a way to make my jump. All I have at my disposition to do this is :
player.y +=
#can be -=, =, +=, etc.
So, do you got any ideas of ways to do this ? I'm not really asking a specific problem, I'd just like to gather some ideas ! And there's even no need to give predone examples, just throw your ideas, like : use this, try with this formula, this function, etc.
Adding some details : what I already tried.
Here is the main solution I tried, pretty basic :
velocity = 3
def input(key):
global velocity
if key == "space":
for i in range(7):
print(velocity)
player.y += velocity
velocity -= 1
velocity = 3
Which is pretty cool, as you had to your height 3, then 2, then 1 (deceleration as your energy lowers), then you add -1, -2, -3 (acceleration due to gravity), and go back to your starting point. Perfect ! But, as said, instantly done. So if I try this :
velocity = 3
def input(key):
global velocity
if key == "space":
for i in range(61):
print(velocity)
player.y += velocity
velocity -= 0.1
velocity = 3
Again, instantly done. And if I try higher and higher intervals, at some point I just get it to lag, no in-between where it's done correctly
Slightly off-topic: You don't want to name your function input() because it shadows the inbuilt input() function.
The problem is that you change the velocity and then iteratively decrement it inside a loop! Because of the way python (or most programming languages, for that matter) works, the program execution moves on to the "draw on screen" step only after it's finished executing your input() function. So when you press a key, here's what your program is doing:
Draw a frame and listen for keypress
Key pressed! Call input() to handle the keypress (let's assume player.y = 0)
Is the key a space? Enter the loop
velocity = 3. Move player up by 3. Decrement velocity. player.y = 3
velocity = 2. Move player up by 2. Decrement velocity. player.y = 5
... and so on until you exit the loop
player.y is 0 again
Draw another frame. Player is at the same place they started, so it looks like nothing happened.
When you add iterations to your loop, this process takes longer (so you see lag), but essentially the same thing happens.
To fix this, you need to add the effect of gravity inside the function that draws your frames. For example, you could set a flag when the jump key is pressed, and if you had a function step() that was called at each timestep of your simulation, you could check if the flag is set and then handle the situation
def user_input(key):
global jump_velocity, is_player_jumping
if key == "space":
is_player_jumping = True
jump_velocity = 3
def step():
global jump_velocity, is_player_jumping
if is_player_jumping:
player.y += jump_velocity
jump_velocity -= 0.1
if player.y == 0: # Player is back on the ground
is_player_jumping = False
This way, you only change the player's location a little bit before the next frame is drawn, and you can actually see the animation.
You first need to know what is the current time step because if you have 2 ms between your frames and 20 ms your need to adapt the amount you get into the player's y position each step.
then it would be great to have a player velocity variable somewhere in addition to its position. Then you would have to decide on a velocity to add instantly to the player when the jump occurs and each time step adds a bit of acceleration down due to gravity.
I'm trying to create a snake algorithm that beats the game on its own, I've gone for the Hamiltonian cycle method with the capability of creating shortcuts, I was trying to create the cycle by using a pathfinding algorithm and compute the longest path where the head of the snake is the start, the tail the end and the 2 blocks in between are walls, the pathfinding library in python uses a matrix to represent the map of pixels, so to generate my matrix I use this block of code:
from pathfinding.core.grid import Grid
from pathfinding.finder.a_star import AStarFinder
from pathfinding.core.diagonal_movement import DiagonalMovement
matrix=[]
row=[]
yi=0
xi=0
while yi<800:
if row != []:
matrix.append(row)
yi += 20
row.clear() #This causes crash
while xi<1400:
row.append(1)
if xi == 40 or xi == 60:
if yi == 20:
row.append(0)
xi += 20
grid = Grid(matrix=matrix)
start = grid.node(4, 1)
end = grid.node(1, 1)
finder = AStarFinder(diagonal_movement=DiagonalMovement.always)
path, runs = finder.find_path(start, end, grid)
print('operations:', runs, 'path length:', len(path))
print(grid.grid_str(path=path, start=start, end=end))
But whenever I run it, it crashes,
I've narrowed it down to the line
row.clear()
but I have no clue why it does this, if I remove it no walls are created, other methods of emptying the list like :row=[] or
while i<len(row):
row.remove(i)
row += 1
give me the same result, I get no error message, nothing prints it just crashes, its even more clear on the entire code because the window displaying the game of snake doesn't display anything and the windows crash window appears, I'm using Windows 10, python 3.8.2
I'm quite new to programming so please excuse my inefficient code, I do it for fun and performance brings me little pleasure, any help is greatly appreciated
I hope I didn't miss anything obvious making you waste time but as long as my code is fixed I'm a happy chappy.
Thanks
Turns out I'm really stupid and forgot to add xi = 0 so it could start the indented loop again instead of just looping into infinity
I am currently in the process of making a new cannon game. How can I make it so that there is just one cannon, on the bottom left hand of the screen?
from graphics import *
from math import sqrt
from math import trunc
def PinR(p,r):
if p.getX()>=r.getP1().getX() and p.getX()<=r.getP2().getX()and p.getY()>=r.getP1().getY() and p.getY()<=r.getP2().getY():
return True;
else:
return False;
def distance(p1,p2):
dx=p1.getX()-p2.getX();
dy=p1.getY()-p2.getY();
dist=sqrt(dx*dx+dy*dy);
return dist;
#parameter
FieldWidth=700;
FieldHeight=700;
GroundDepth=75;
BallSize=10;
OriginalSpeed=4;
FieldBackground="brown";
FieldBorder="brown";
tickLength=800000;
buttonSize=8;
# number of cannons and balls
numBalls=4;
# initial cannon power
explosionStrength=30;
# intial gravitational constant
g=1;
# clock tick delay
delay=0.05;
#Create field
Field=GraphWin("B",FieldWidth,FieldHeight);
Field.setBackground(FieldBackground);
#set of balls
spacing=FieldWidth/(numBalls);
ball=[];
for b in range (0,numBalls):
newball=Circle(Point(spacing*b+spacing//2,FieldHeight-GroundDepth),BallSize);
newball.setFill("black");
newball.draw(Field);
ball.append(newball);
#cannon
cannon=[]
for c in range (0,numBalls):
newCannon=Rectangle(Point(spacing*c+spacing//2-BallSize,FieldHeight-GroundDepth-BallSize*5),
Point(spacing*c+spacing//2+BallSize,FieldHeight-GroundDepth+BallSize));
newCannon.setFill("black");
newCannon.draw(Field);
cannon.append(newCannon);
#set of button groups (fire, powerup, powerdown)
fire=[];
for f in range (0,numBalls):
newbutton=Rectangle(Point(spacing*f+spacing//2-buttonSize//2,FieldHeight-GroundDepth-BallSize),
Point(spacing*f+spacing//2+buttonSize//2,FieldHeight-GroundDepth-BallSize+buttonSize));
newbutton.setFill("red");
newbutton.draw(Field);
fire.append(newbutton);
#wall
#target(red,white,red,white)
balldistance=20;
ball1=Circle(Point(FieldWidth//2-20,FieldHeight//2+20),BallSize);
ball1.setFill("red");
ball1.draw(Field);
The reason you get 4 cannons is that you're doing this:
for c in range (0,numBalls):
… where numBalls is 4, and you create a new cannon each time through the loop.
Presumably with only 1 cannon you also only want one cannon ball and one shot, so just set numBalls = 1 instead of numBalls = 4.
However, it might make more sense to simplify the program while you're at it. Replace the lists of 4 cannons with a single cannon, get rid of the loop, do the same for the 4 balls, etc. Then you can also simplify the layout rules—no need for a spacing variable to configure how far apart the cannons are if there's only 1 of them. And so on. This might make it easier for you to understand how the program works—and figuring out how to simplify it might similarly be beneficial on its own.
And if you want to change its position, that's being set in this line:
newCannon=Rectangle(Point(spacing*c+spacing//2-BallSize,FieldHeight-GroundDepth-BallSize*5),
Point(spacing*c+spacing//2+BallSize,FieldHeight-GroundDepth+BallSize));
So, you can tweak the various constants (which all seem to have pretty reasonable names) to get the result you want—or, of course, just hardcode the position you want instead of calculating it.