wxPython Rendering issues, very slow and crashes, not sure why - python

I have been creating a simple tile based game to help me learn python and wx python. For starters I wanted to create my own 'world' and to test the simple map generator I made, I bound the return key to generate a new map and display it.
That is when I ran into this problem. It slows down a lot every time you click return, renders each tile line by line (which is obviously slow and inefficient) and eventually just freezes.
I am quite a novice programmer and have never dealt with any form of GUI so this is all very new to me, please bear with me! I can guess that the way I have things set up is very taxing for the machine, and that perhaps I'm causing a lot of recursion. I'm simply unaware. Also I'm not too familiar with OOP so I just follow examples to create my classes (hence why I only have 1 massive class that handles everything, Im not too sure on all the '__ something__' functions.)
Here is all the code I have written so far, please ignore commented-out sections, they are for future functions etc:
import wx
import random
#main screen class, handles all events within the main screen
class MainScreen(wx.Frame):
hMap = []
tTotalX = 50
tTotalY = 50
def __init__(self, *args, **kwargs):
#This line is equivilant to wx.Frame.__init__('stuff')
super(MainScreen, self).__init__(None, -1, 'You shouldnt see this', style = wx.SYSTEM_MENU | wx.CAPTION | wx.CLOSE_BOX)
self.renderScreen()
def genMap(self,tTotalX,tTotalY):
count1 = 0
count2 = 0
self.hMap = []
while count1 < tTotalY:
count2 = 0
newrow = []
while count2 < tTotalX:
newrow.append(random.randint(1,120))
count2 += 1
self.hMap.append(newrow)
count1 += 1
self.smooth(tTotalX, tTotalY)
self.smooth(tTotalX, tTotalY)
def smooth(self, tTotalX, tTotalY):
countx = 0
county = 0
while county < tTotalY:
countx = 0
while countx < tTotalX:
above = county - 1
below = int(county + 1)
east = int(countx + 1)
west = int(countx - 1)
if east >= tTotalX:
east = 0
if west < 0:
west = tTotalX -1
teast = self.hMap[county][east]
twest = self.hMap[county][west]
if above < 0 or below >= tTotalY:
smooth = (self.hMap[county][countx] + teast + twest)/3
else:
tabove = self.hMap[above][countx]
tbelow = self.hMap[below][countx]
smooth = (self.hMap[county][countx] + tabove + tbelow + teast + twest)/5
self.hMap[countx][county] = int(smooth)
countx += 1
county += 1
def getTileType(self, coordX, coordY, totalX, totalY):
#this is the part of map creation, getting tile type based on tile attributes
tType = ''
height = self.hMap[coordX][coordY]
#the below values are all up to tweaking in order to produce the best maps
if height <= 55:
tType = 'ocean.png'
if height > 55:
tType = 'coast.png'
if height > 60:
tType = 'grassland.png'
if height > 75:
tType = 'hills.png'
if height > 80:
tType = 'mountain.png'
if tType == '':
tType = 'grassland.png'
return tType
#render the main screen so that it dislays all data
def renderScreen(self):
frameSize = 810 #Size of the game window
tTotalX = self.tTotalX #the dimensions of the tile display, setting for in-game coordinates
tTotalY = self.tTotalY
#tsTiny = 1 #ts = Tile Size
#tsSmall = 4
tsMed = 16
#tsLrg = 32
#tsXlrg = 64
tsCurrent = tsMed #the currently selected zoom level, for now fixed at tsMed
pposX = 0 #ppos = Pixel Position
pposY = 0
tposX = 0 #tpos = tile position, essentially the tile co-ordinates independent of pixel position
tposY = 0
#The below is just an example of how to map out the grid, it should be in its own function in due time
self.genMap(tTotalX, tTotalY)
while tposY < tTotalY: #loops through all y coordinates
tposX = 0
while tposX < tTotalX: #loops through all x coordinates
pposX = tposX*tsCurrent
pposY = tposY*tsCurrent
tiletype = self.getTileType(tposX,tposY,tTotalX,tTotalY)
img = wx.Image(('F:\First Pass\\' + str(tiletype)), wx.BITMAP_TYPE_ANY).ConvertToBitmap()
wx.StaticBitmap(self, -1, img, (pposX, pposY))#paints the image object (i think)
tposX += 1
tposY += 1
self.Bind(wx.EVT_KEY_DOWN, self.onclick)
self.SetSize((frameSize-4, frameSize+16))
self.SetBackgroundColour('CYAN')
self.Centre()
self.SetTitle('Nations First Pass')
#string = wx.StaticText(self, label = 'Welcome to Nations, First Pass', pos = (tTotalX*tsCurrent/2,tsCurrent*tTotalY/2))
#string.SetFont(wx.SystemSettings_GetFont(wx.SYS_SYSTEM_FONT))
self.Show()
def onclick(self, e):
key = e.GetKeyCode()
if key == wx.WXK_RETURN:
self.renderScreen()
#game loop
def main():
app = wx.App()
MainScreen(None)
app.MainLoop()
if __name__ == '__main__':
main()
You will need to fabricate your own 'ocean.png' 'coast.png' 'grassland.png' 'hills.png' and 'mountain.png' (they need to be 16x16 pixels) or you can use mine from the Imgur link:
http://imgur.com/a/uFxfn
Also please change the file path in the img code as appropriate. I need to figure out how to set that to do it itself as well but thats another challenge for another day.
If you have any insight into this I'd be very appreciative.

You are creating a new set of wx.StaticBitmaps each time renderScreen is called, and you are not deleting them before creating a new set. After a while you'll have gobs of widgets stacked up with the old ones no longer visible, but still there consuming resources. At a minimum you should change things so your program makes only one set of wx.StaticBitmaps, keeps track of them, and then call their SetBitmap method when you want to change them.
For even better performance you should forget about the StaticBitmaps and draw the images yourself in an EVT_PAINT handler. StaticBitmaps are intended to be "static", IOW not changing very much. Instead you can implement an EVT_PAINT handler for the window and it will be called whenever the window needs to be redrawn, and you can trigger new redraws simply by calling the window's Refresh method.

Related

Unable to change Tkinter frame color

I am trying to create something simple using Tkinter. In it, I am creating a series of frames and then using frame.grid() putting them inside of another frame. I am able to retrieve the frame and print out information about it, but I am unable to change the color.
I have looked through other posts and was unable to find a solution. I have tried:
Updating the color using .config(bg=color)/.configure(bg=color)
Updating the color using frame['bg']=
Placing the function before or after the creation of the frames.
I am able to edit the color of the frames when I am calling a function through the <Button-1> event, but when I am running a different function, it is unable to change the colors.
I am using Replit, here is the important code:
window = tk.Tk()
window.wm_attributes('-fullscreen',True)
window.config(bg='#111111')
height = window.winfo_screenheight()
width = window.winfo_screenwidth()
#window.geometry(f'{height}x{height}')
rows = height//gridSize
columns = height//gridSize
frame = tk.Frame(master=window, bg='#111111', width=height, height=height, pady=10)
frame.pack(side=tk.BOTTOM)
def getNumLiveNeighbors(cellRow, cellColumn):
numLiveNeighbors = 0
# print(cellRow, cellColumn)
r = -2
for i in range(3):
c = -2
r += 1
for a in range(3):
c += 1
if(r==0)and(c==0):
continue
nRow, nCol = cellRow+r, cellColumn+c
if(((nRow<0)or(nRow>=gridSize))or(nCol<0)or(nCol>=gridSize)):
continue
neighbor = frame.grid_slaves(row=nRow, column = nCol)[0]
if(neighbor['bg']=='#ffffff'):
numLiveNeighbors += 1
#neighbor.config(bg='#252525')
return numLiveNeighbors
def toggleColor(event):
widget = event.widget
if(widget['bg']=='#ffffff'):
widget.config(bg='#222222')
else:
widget.config(bg='#ffffff')
info = widget.grid_info()
print(getNumLiveNeighbors(info['row'], info['column']))
print(event.x, event.y)
return
for i in range(gridSize):
window.columnconfigure(i, weight = 1)
window.rowconfigure(i, weight = 1)
for i in range(gridSize):
cellGrid[i] = {}
for j in range (gridSize):
box = tk.Frame(master=frame, bg='#222222', relief=tk.RAISED, width=columns, height=columns)
cellGrid[i][j] = box
box.bind('<Button-1>', toggleColor)
box.grid(row=i,column=j,sticky='nsew')
def runGame(event):
frameCount = 0
print('Running game!')
while(True):
print(f'Frame #{frameCount}')
frameCount += 1
for row in range(gridSize):
for col in range(gridSize):
time.sleep(0.5)
cell = frame.grid_slaves(row=row, column=col)[0]
numNeighbors = getNumLiveNeighbors(row, col)
print(numNeighbors)
if(numNeighbors<2):
print('Less than 2, do nothing')
#cell.config(bg='#222222')
elif(numNeighbors==3):
print('3 Neighbors, make cell alive!')
cell['bg']='#ffffff'
elif(numNeighbors>3):
print('Greater than 3 neighbors, kill the cell')
cell['bg']='#222222'
print(cell['bg'])
window.bind('<Return>', runGame)
If thats too much to read I can break it down a bit more and explain what I'm doing with each function/line.
The main goal of this was to create a cellular automata like Conway's Game of Life, but I've been stuck trying to update the color of the "cells".
You CANNOT use an infinite loop like that. Tk, like all GUI frameworks, is event driven. When you create a window or make a change, that doesn't actually do any drawing. It just sends a message. That message will be dispatched and acted upon when you get back to the main loop. In your case, you never exit your loop, so your handler never returns, the main loop never runs, and your UI freezes.
Instead of an infinite loop with time.sleep(0.5), you need to use window.after to request a timer callback. Do one generation and return (after calling window.after again). That way, the main loop stays in control, and all will be well.

Python CodeSkulptor Pause Drawing from Inside For Loop

I want to build some visualizations for searching algorithms (BFS, A* etc.) within a grid.
My solution should show each step of the algorithm using CodeSkulptor simplegui (or the offline version using SimpleGUICS2Pygame.)
I've made a version which highlights all the cells visited by changing their color, but I've run into trouble trying to make the path display step-by-step with a time delay between each step.
I've extracted the essence of the problem and created a minimal example representing it in the code below, also run-able online here: http://www.codeskulptor.org/#user47_jB2CYfNrH2_2.py
What I want is during the change_colors() function, for there to be a delay between each iteration.
CodeSkulptor doesn't have time.sleep() available, and I don't think it would help anyway.
CodeSkulptor does have timers available, which might be one solution, although I can't see how to use one in this instance.
Code below:
import time
try:
import simplegui
except ImportError:
import SimpleGUICS2Pygame.simpleguics2pygame as simplegui
simplegui.Frame._hide_status = True
TITLE = "TEST"
FRAME_WIDTH = 400
FRAME_HEIGHT = 400
DELAY = 10
class Square:
"""This class represents a simple Square object."""
def __init__(self, size, pos, pen_size=2, pen_color="red", fill_color="blue"):
"""Constructor - create an instance of Square."""
self._size = size
self._pos = pos
self._pen_size = pen_size
self._pen_color = pen_color
self._fill_color = fill_color
def set_color(self, color):
self._fill_color = color
def get_color(self):
return self._fill_color
def is_in(self, pos):
"""
Determine whether coordinates are within the area of this Square.
"""
return self._pos[0] < pos[0] < self._pos[0] + self._size and self._pos[1] < pos[1] < self._pos[1] + self._size
def draw(self, canvas):
"""
calls canvas.draw_image() to display self on canvas.
"""
points = [(self._pos[0], self._pos[1]), (self._pos[0] + self._size, self._pos[1]),
(self._pos[0] + self._size, self._pos[1] + self._size), (self._pos[0], self._pos[1] + self._size)]
canvas.draw_polygon(points, self._pen_size, self._pen_color, self._fill_color)
def __str__(self):
return "Square: {}".format(self._pos)
def draw(canvas):
for square in squares:
square.draw(canvas)
def change_colors():
for square in squares:
# time.sleep(1) # Not implemented in CodeSkulptor and would'nt work anyway
square.set_color("green")
frame = simplegui.create_frame(TITLE, FRAME_WIDTH, FRAME_HEIGHT)
frame.set_draw_handler(draw)
width = 20
squares = []
for i in range(10):
squares.append(Square(width, (i * width, 0)))
change_colors()
frame.start()
Any help appreciated.
Yes, you need to use a timer. Something like this:
I = 0
def change_next_color():
if I < len(squares):
squares[I].set_color("green")
global I
I += 1
timer = simplegui.create_timer(1000, change_next_color)
timer.start()
http://www.codeskulptor.org/#user47_udyXzppCdw2OqdI.py
I also replaced
simplegui.Frame._hide_status = True
by simplegui.Frame._hide_controlpanel = True
https://simpleguics2pygame.readthedocs.io/en/latest/simpleguics2pygame/frame.html#SimpleGUICS2Pygame.simpleguics2pygame.frame.Frame._hide_controlpanel
See also _keep_timers option of SimpleGUICS2Pygame to help you:
https://simpleguics2pygame.readthedocs.io/en/latest/simpleguics2pygame/frame.html#SimpleGUICS2Pygame.simpleguics2pygame.frame.Frame._keep_timers
Possible improvements:
Find a better solution that don't use a global counter.
Stop timer when all work is finished.

Looping a function until another function is called

I am making a menu that runs on an LCD screen powered by a Raspberry Pi. I am trying to use the threading module to make the text, on the LCD, update until the menu position changes.
The menu is made up of a list of functions that are called when the menu position is changed. The switch_menu() function is called from outside the class, using an event handler, and is used to call the correct menu function. With some of these functions(item2); I want them to loop, and with others(item1); just display static text. The important thing is that they stop looping when switch_menu() is called again. How can I do this?
(here is a simplified version of my code)
class Menu:
def __init__(self):
self.LCD = Adafruit_CharLCD()
self.m_pos = 0
self.items = [self.item1,self.item2]
self.switch_menu(0)
def switch_menu(self,operation):
# 2. And here I want to stop it.
m_pos = self.m_pos
pos = m_pos
max_pos = len(self.items) - 1
m_pos = self.loop_selection(pos,max_pos,operation)
# 1. Here I want to start looping the function below.
self.items[m_pos]()
self.m_pos = m_pos
def loop_selection(self,pos,max_pos,operation):
if pos >= max_pos and operation == 1:
pos = 0
elif pos <= 0 and operation == -1:
pos = max_pos
else:
pos += operation
return pos
def item1(self):
self.LCD.clear()
text = "item1"
self.LCD.message(text)
def item2(self):
while True:
self.LCD.clear()
text = "item2"
self.LCD.message(text)
time.sleep(10)
There are many ways to achieve this, one simple way is to make the while loop in a variable and then set it to False outside the loop (for example, when calling switch_menu) once you want to stop it. Just beware of any race conditions that may be caused, of which I can't talk much more about since I don't know the rest of your code.
Typical, I have been trying to get this to work for days and as soon as I post a question; I find the answer.
Here is where I found my answer:
Stopping a thread after a certain amount of time
And this is what I did to make it work:
class Menu:
def __init__(self):
self.LCD = Adafruit_CharLCD()
self.m_pos = 0
self.items = [self.item1,self.item2]
self.switch_menu(0)
def switch_menu(self,operation):
try:
self.t_stop.set()
except:
pass
m_pos = self.m_pos
pos = m_pos
max_pos = len(self.items) - 1
m_pos = self.loop_selection(pos,max_pos,operation)
item = self.items[m_pos][0]
self.t_stop = threading.Event()
self.t = threading.Thread(target=item,args=(1,self.t_stop))
self.t.start()
self.m_pos = m_pos
def loop_selection(self,pos,max_pos,operation):
if pos >= max_pos and operation == 1:
pos = 0
elif pos <= 0 and operation == -1:
pos = max_pos
else:
pos += operation
return pos
def item1(self,arg):
while not stop_event.is_set():
text = "item1"
self.LCD.clear()
if not stop_event.is_set(): self.LCD.message(text)
stop_event.wait(10)
def item2(self,arg):
while not stop_event.is_set():
text = "item2"
self.LCD.clear()
if not stop_event.is_set(): self.LCD.message(text)
stop_event.wait(10)
I used a try/except to bypass the initial execution of switch_menu():
try:
self.t_stop.set()
except:
pass
I check the condition a second time as a workaround, to prevent race conditions:
if not stop_event.is_set(): self.LCD.message(text)
And I don't know why I had to pass in an argument when creating a thread, but it gave me errors when I didn't:
self.t = threading.Thread(target=item,args=(1,self.t_stop))
I know it needs some tidying up, but it works. If anyone has a more elegant solution feel free to post it.

Fade Between Two Music Tracks in-progress in Pygame

My intention is to have two music tracks, which are similar in nature, fade between each other at various times. When such a fade occurs, one music track should fade from full volume to muted in a short period of time, and, simultaneously, the other track should fade from 0 to 100 and continue playing from the same time index. They must be able to do this dynamically at any time - when a certain action occurs, the fade will occur and the new track will start playing at the same position that the other one left off at.
This might be plausible by either using volume manipulation or by starting and stopping the music (however, it appears that only a "fadeout" option exists, and there is a lack of a "fadein" option). How can I do this? What is the best method, if any, that exists? If it is impossible using Pygame, alternatives to Pygame are acceptable.
This isn't exactly an answer to the question, but for future-googlers I wrote a script to fade-in my music from volume 0 in the morning and this is what I used:
max_volume = 40
current_volume = 0
# set the volume to the given percent using amixer
def set_volume_to(percent):
subprocess.call(["amixer", "-D", "pulse", "sset", "Master",
str(percent) + "%", "stdout=devnull"])
# play the song and fade in the song to the max_volume
def play_song(song_file):
global current_volume
print("Song starting: " + song_file)
pygame.mixer.music.load(song_file)
pygame.mixer.music.play()
# gradually increase volume to max
while pygame.mixer.music.get_busy():
if current_volume < max_volume:
set_volume_to(current_volume)
current_volume += 1
pygame.time.Clock().tick(1)
play_song("foo.mp3")
Try this, it's pretty straight forward..
import pygame
pygame.mixer.init()
pygame.init()
# Maybe you can subclass the pygame.mixer.Sound and
# add the methods below to it..
class Fader(object):
instances = []
def __init__(self, fname):
super(Fader, self).__init__()
assert isinstance(fname, basestring)
self.sound = pygame.mixer.Sound(fname)
self.increment = 0.01 # tweak for speed of effect!!
self.next_vol = 1 # fade to 100 on start
Fader.instances.append(self)
def fade_to(self, new_vol):
# you could change the increment here based on something..
self.next_vol = new_vol
#classmethod
def update(cls):
for inst in cls.instances:
curr_volume = inst.sound.get_volume()
# print inst, curr_volume, inst.next_vol
if inst.next_vol > curr_volume:
inst.sound.set_volume(curr_volume + inst.increment)
elif inst.next_vol < curr_volume:
inst.sound.set_volume(curr_volume - inst.increment)
sound1 = Fader("1.wav")
sound2 = Fader("2.wav")
sound1.sound.play()
sound2.sound.play()
sound2.sound.set_volume(0)
# fading..
sound1.fade_to(0)
sound2.fade_to(1)
while True:
Fader.update() # a call that will update all the faders..
Pseudocode:
track1 = ...
track2 = ...
track1.play_forever()
track1.volume = 100
track2.play_forever()
track2.volume = 0
playing = track1
tracks = [track1, track2]
def volume_switcher():
while True:
playing.volume = min(playing.volume + 1, 100)
for track in tracks:
if track != playing:
track.volume = max(track.volume - 1, 100)
time.sleep(0.1)
Thread(target=volume_switcher).start()
So it looks like what you want to do in pygame is create two 'Sound' objects, and create a linear interpolation on the volume between the two of them.
I would create two vectors, each from [0,100], and relate them inversely with some constant.
So when sound A is at 100, sound b is at 0. Then when an action occurs, you modify the constant.
t=0
A: [0 ... 100]
B: [0 ... 100]
t=1
ACTION
t=1.1
A:[0 .. 50 .. 100]
B:[0 .. 50 .. 100]
t=2
A:[0 ... 100]
B:[0 ... 100]
Now some code. I'm not familiar with pygame, but this should put you on the right track.
class Song(object):
def __init__(self, songfilename):
self.song = pygame.mixer.Sound(songfilename)
def setVolume(self, somenumber):
#number validation
#possibly do some volume curve here if you wanted
self.song.set_volume(somenumber)
class SongFader(object):
def __init__(self, song1, song2):
self.song1 = song1
self.song2 = song2
self.__xAxisMax = 100
self.__xAxisMin = 0
def fade(self, xaxis):
assert(self.__xAxisMin <= xaxis <= self.__xAxisMax)
#could be any numbers you want.
#i chose 0-100 for convenience
self.song1.setVolume(xaxis)
self.song2.setVolume(self.__xAxisMax-xaxis)
song1 = Song('Song1.wav')
song2 = Song('Song2.wav')
fader = SongFader(song1, song2)
#Inside some event loop when you action is triggered
fader.fade(100) #Only song2 is playing
fader.fade(50) #Songs are evenly split
fader.fade(0) #Only left song is playing
edit
The linear interpolation is probably the more important concept here, so i have modified the fader class, with inspiration from Eric 's thread idea.
class SongFader(object):
def __init__(self, song1, song2):
self.song1 = song1
self.song2 = song2
self.lefttoright = False
self.starttime = 0
self.endtime = 0
def fade(self, starttime, fadeleft):
self.lefttoright = fadeleft == True #Being verbose here
self.starttime = starttime #assuming time is in millis
self.endtime = starttime + 1000
Thread(target = self.fadeHelper).start()
#this is where you define how the two songs are faded
def fadeHelper(self):
#if using thread, make sure you mutex the 'self.' variables
starttime = self.starttime
endtime = self.endtime
lefttoright = self.lefttoright
while starttime < endtime:
fadevalue = (starttime - endtime) / 1000 #a val between [0,1]
if lefttoright:
self.song1.setVolume(fadevalue)
self.song2.setVolume(1-fadevalue)
else:
self.song1.setVolume(1-fadevalue)
self.song2.setVolume(fadefalue)
starttime = getGameTimeFromSomewhere()

Error with pymunk space.remove method

I have a ball generator, that "generates" and adds balls(circles) to the simulation.
The ball is to be removed when it hits a static poly in list s_boxes.
This is done by a collision handler ball_wall_collision.
The Error:
The following pop-up window does what it's name says, it pops-up
My code:
Ball Generator
class BallGenerator:
def __init__(self, min_y, max_y, x):
self.min = min_y
self.max = max_y
self.x = x
self.counter = 0
def bowl(self, balls):
global ball_bowled
y = random.randint(self.min, self.max)
pos = to_pymunk((self.x,y))
r = 10
m = 15
i = pm.moment_for_circle(m, 0, r)
b = pm.Body(m,i)
b.position = pos
f_x = random.randint(-600000,-400000)
b.apply_force( (f_x,0.0),(0,0) )
ball = pm.Circle(b, r)
ball.elasticity = 0.75
ball.friction = 0.95
balls.append(ball)
space.add(ball,b)
print 'bowled'
ball_bowled += 1
def handle(self, balls):
if self.counter == FPS:
self.bowl(balls)
self.counter = 0
self.counter += 1
Collision handler
def ball_wall_collision(space, arb, balls, s_boxes):
shapes = arb.shapes
boxes = [box[0] for box in s_boxes] # Get walls
ball = None
wall = None
for ba in balls:
if ba in shapes:
ball = ba
break
for box in boxes:
if box in shapes:
wall = box
break
if wall and ball:
print 'removing'
space.remove(ball, ball.body) # Where the runtime problem happens
balls.remove(ball)
print 'removed'
return False
else:
return True
space.add_collision_handler(0,0,begin=ball_wall_collision,
balls=balls,s_boxes=s_boxes) # Other args to function
What am I doing wrong in the collision handling??
Am I missing something in the call to space.remove?
Is the function not working as I want it to??
Or is the error elsewhere (which I don't think it is)...
It looks like the problem is that you try to remove objects from the space in a collision handler during the simulation step.
Instead you can try with either manually collect all the balls into a list and then call remove after the step, or queue up removes with the post step callback like this:
space.add_post_step_callback(space.remove, ball)
space.add_post_step_callback(space.remove, ball.body)
(untested code)
I should probably try and make this more obvious in the API docs.. I wonder if it would be a good idea to automatically schedule the remove until end of step, or the less intrusive option, trigger a assert in python so you dont get the c++ error.

Categories

Resources