Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Imported image as Scene background
-
@Robert_Tompkins, for performance, you could consider showing the same or different picture 1-3 times on top of the background pic, as a semi-transparent layer, and maybe varying the location and transparency of these pictures. This could make the background ”live” without managing hundreds of individual objects.
-
@mikael
Man, you’re a genius. How do I not think of these things??
Yes, I will give this a try.
When I set my background_color == black, I can clearly see the number of individual objects being created representing stars. So even if I use 20 pictures, I assume I’ll see a difference.Before I do the tiling, I’ll remove the stars entirely and use a simple black background color to get a baseline with my current code. Thanks for the info and idea!
-
Alright, so I did see significant improvement after making more changes. However... I would like some more info from those that can answer.
I have an upgrade for my ‘rocket’ weapon that was meant to just explode and intersect with nearby meteors to trigger a call to the function handling this event. However, I went a different route as I couldn’t figure out how to do this. Here is effectively what I did:
Created a new Class:class MiniRocket (SpriteNode): def __init__(self, **kwargs): img = 'plf:LaserPurpleDot' SpriteNode.__init__(self, img, scale = 0.05, **kwargs)
Here is my function that handles projectile collisions ( it is called via update() ):
def check_laser_collisions(self): for projectile in list(self.projectiles): if not projectile.parent: self.projectiles.remove(projectile) continue for item in self.items: if not isinstance(item, Meteor): if not isinstance(item, Meteor2): if not isinstance(item, Meteor3): if not isinstance(item, Meteor4): if not isinstance(item, Meteor5): if not isinstance(item, Meteor6): if not isinstance(item, MeteorMedium): if not isinstance(item, MeteorSmall): if not isinstance(item, MeteorTiny): continue if item.destroyed: #print("Entered if item.destroyed:") continue #print("Reached if projectile.position in item.frame:") if projectile.position in item.frame: if isinstance(projectile, Rocket): # sound.play_effect('arcade:Explosion_2') self.destroy_meteor(item, 1) self.projectiles.remove(projectile) projectile.remove_from_parent() #m = SpriteNode(explosionTexture, scale = 0.50, color = '#ffaf57', parent=self) #m.position = projectile.position #m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) if isinstance(projectile, MiniRocket): #sound.play_effect('arcade:Explosion_2') self.destroy_meteor(item) projectile.collisionsLeft -= 1 if projectile.collisionsLeft <= 0: self.projectiles.remove(projectile) projectile.remove_from_parent() #m = SpriteNode(explosionTexture, scale = 0.25, color = '#ffaf57', parent=self) #m.position = projectile.position #m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) if isinstance(projectile, Laser): self.destroy_meteor(item) self.destroy_meteor(item) projectile.collisionsLeft -= 2 if projectile.collisionsLeft <= 0: self.projectiles.remove(projectile) projectile.remove_from_parent() else: self.destroy_meteor(item) #self.projectiles.remove(projectile) #projectile.remove_from_parent() break
Here is destroy_meteor() called above via “self.destroy_meteor(item, 1)”
The second argument ‘1’ is used to indicate that a rocket collision occurred, which is different from other collisions, like from a ‘laser’, or ‘miniRocket’. It uses this to know it should create miniRockets.
def destroy_meteor(self, meteor, isRocket = None): global brownMeteorMed global explosionTexture global smokeTextureMed global smokeTextureSmall global starTextureSilver #print(f'Entered destroy_meteor() with meteor: {meteor}') if isinstance(meteor, Meteor): # sound.load_effect('arcade:Explosion_2') # sound.play_effect('arcade:Explosion_2') m = SpriteNode(explosionTexture, scale = 0.25, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() if isinstance(meteor, Meteor2): # sound.play_effect('arcade:Explosion_7') if random.random() < 0.25 * self.stageNumber: if self.stageNumber < 5: meteor.livesRemaining -= 250 else: if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: for i in range(2): meteorMedium = MeteorMedium(parent=self) meteorMedium.destroyed = False meteorMedium.livesRemaining = 10 * self.stageNumber meteorMedium.position = meteor.position + (random.uniform(-30, 30), (-20 + random.uniform(-5, 5))) downwardAngle = random.uniform(pi, pi*2) dx, dy = cos(downwardAngle) * 80, sin(downwardAngle) * 80 d = random.uniform(8.0, 15.0) actions = [A.move_to(random.uniform(0, self.size.w), -25, d), A.remove()] meteorMedium.run_action(A.sequence(actions)) self.items.append(meteorMedium) m = SpriteNode(explosionTexture, scale = 0.25, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() else: if meteor.livesRemaining <= (20 * self.stageNumber) * 0.25: meteor.alpha = 0.25 elif meteor.livesRemaining <= (20 * self.stageNumber) * 0.50: meteor.alpha = 0.5 elif meteor.livesRemaining <= (20 * self.stageNumber) * 0.75: meteor.alpha = 0.75 if isinstance(meteor, MeteorMedium): # sound.play_effect('arcade:Explosion_7') if random.random() < 0.20 * self.stageNumber: if self.stageNumber < 5: meteor.livesRemaining -= 250 else: if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: m = SpriteNode(smokeTextureSmall, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() ## REMOVED BELOW BLOCK TO REDUCE STUTTERING FROM MANY OBJECTS ON SCREEN """ for i in range(2): meteorSmall = MeteorSmall(parent=self) meteorSmall.destroyed = False meteorSmall.livesRemaining = 10 * self.stageNumber meteorSmall.position = meteor.position downwardAngle = random.uniform(pi, pi*2) dx, dy = cos(downwardAngle) * 80, sin(downwardAngle) * 80 d = random.uniform(7.0, 12.0) actions = [A.move_to(random.uniform(0, self.size.w), -25, d), A.remove()] meteorSmall.run_action(A.sequence(actions)) self.items.append(meteorSmall) meteor.remove_from_parent() m = SpriteNode(smokeTextureSmall, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() """ else: # sound.play_effect('arcade:Hit_1') if meteor.livesRemaining <= (10 * self.stageNumber) * 0.25: meteor.alpha = 0.25 elif meteor.livesRemaining <= (10 * self.stageNumber) * 0.50: meteor.alpha = 0.5 elif meteor.livesRemaining <= (10 * self.stageNumber) * 0.75: meteor.alpha = 0.75 if isinstance(meteor, MeteorSmall): # sound.play_effect('arcade:Explosion_7') if random.random() < 0.15 * self.stageNumber: if self.stageNumber < 5: meteor.livesRemaining -= 250 else: if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: ############################################################################################ ############################################################################################ m = SpriteNode(smokeTextureSmall, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() ## REMOVED BELOW BLOCK TO REDUCE STUTTERING FROM MANY OBJECTS ON SCREEN """ for i in range(2): meteorTiny = MeteorTiny(parent=self) meteorTiny.destroyed = False meteorTiny.livesRemaining = 10 * self.stageNumber meteorTiny.position = meteor.position + (random.uniform(-100, 25), random.uniform(-5, 5)) meteorTiny.scale = 0.5 downwardAngle = random.uniform(pi, pi*2) dx, dy = cos(downwardAngle) * 80, sin(downwardAngle) * 80 d = random.uniform(6.0, 10.0) actions = [A.move_to(random.uniform(0, self.size.w), -25, d), A.remove()] meteorTiny.run_action(A.sequence(actions)) self.items.append(meteorTiny) m = SpriteNode(smokeTextureSmall, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() """ else: # sound.play_effect('arcade:Hit_1') if meteor.livesRemaining <= (10 * self.stageNumber) * 0.25: meteor.alpha = 0.25 elif meteor.livesRemaining <= (10 * self.stageNumber) * 0.50: meteor.alpha = 0.5 elif meteor.livesRemaining <= (10 * self.stageNumber) * 0.75: meteor.alpha = 0.75 if isinstance(meteor, MeteorTiny): if self.stageNumber < 5: meteor.livesRemaining -= 50 else: if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: meteor.destroyed = True meteor.remove_from_parent() #self.items.remove(meteor) if isinstance(meteor, Meteor3): if self.stageNumber < 5: meteor.livesRemaining -= 50 else: if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: # sound.play_effect('arcade:Explosion_2') m = SpriteNode(explosionTexture, scale = 0.25, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() else: # sound.play_effect('arcade:Hit_1') if meteor.livesRemaining <= (10 * self.stageNumber) * 0.25: meteor.alpha = 0.25 elif meteor.livesRemaining <= (10 * self.stageNumber) * 0.50: meteor.alpha = 0.5 elif meteor.livesRemaining <= (10 * self.stageNumber) * 0.75: meteor.alpha = 0.75 if isinstance(meteor, Meteor4): if self.stageNumber < 5: meteor.livesRemaining -= 100 else: if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: # sound.play_effect('arcade:Explosion_2') m = SpriteNode(explosionTexture, scale = 0.50, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() else: # sound.play_effect('arcade:Hit_1') if meteor.livesRemaining <= (20 * self.stageNumber) * 0.25: meteor.alpha = 0.25 elif meteor.livesRemaining <= (20 * self.stageNumber) * 0.50: meteor.alpha = 0.5 elif meteor.livesRemaining <= (20 * self.stageNumber) * 0.75: meteor.alpha = 0.75 if isinstance(meteor, Meteor5): if self.stageNumber < 5: meteor.livesRemaining -= 250 else: if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: # sound.play_effect('arcade:Explosion_2') m = SpriteNode(explosionTexture, scale = 0.75, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() else: # sound.play_effect('arcade:Hit_1') if meteor.livesRemaining <= (40 * self.stageNumber) * 0.25: meteor.alpha = 0.25 elif meteor.livesRemaining <= (40 * self.stageNumber) * 0.50: meteor.alpha = 0.5 elif meteor.livesRemaining <= (40 * self.stageNumber) * 0.75: meteor.alpha = 0.75 if isinstance(meteor, Meteor6): if self.stageNumber < 5: meteor.livesRemaining -= 250 else: if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: # sound.play_effect('arcade:Explosion_2') m = SpriteNode(explosionTexture, scale = 1.00, parent=self) m.position = meteor.position m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) meteor.destroyed = True meteor.remove_from_parent() else: # sound.play_effect('arcade:Hit_1') if meteor.livesRemaining <= (80 * self.stageNumber) * 0.25: meteor.alpha = 0.25 elif meteor.livesRemaining <= (80 * self.stageNumber) * 0.50: meteor.alpha = 0.5 elif meteor.livesRemaining <= (80 * self.stageNumber) * 0.75: meteor.alpha = 0.75 #classmethod Action.move_to(x, y[, duration, timing_mode]) #Creates an action that moves a node to a new position. #actions = [A.move_to(self.ship.position[0], self.ship.position[1], d, TIMING_LINEAR), A.remove()] #actions = [A.move_by(0, -(self.size.h + 30), d), A.remove()] #meteor.run_action(A.sequence(actions)) if isRocket == 1: fragmentsToCreate = round(self.rocketFragmentsLevel) fragmentMovementSpeed = 7.5 - (self.rocketFragmentsLevel * 0.1) if fragmentMovementSpeed <= 1.0: fragmentMovementSpeed = 1.0 #for i in range(fragmentsToCreate): for i in range(fragmentsToCreate): miniRocket = MiniRocket(parent=self) miniRocket.collisionsLeft = self.rocketFragmentPenetrationLevel miniRocket.scale = 0.25 miniRocket.color = '#07ff1c' miniRocket.z_position = 0 #miniRocket.alpha = 0.25 miniRocket.position = meteor.position #miniRocket.position = meteor.position + (random.uniform(-30, 30), (-20 + random.uniform(-5, 5))) fullCircle = random.uniform(0, pi*2) #upwardAngle = random.uniform(0, pi) dx, dy = cos(fullCircle) * 250, sin(fullCircle) * 250 #d = 1.25 #miniRocket.rotation = fullCircle actions = (A.sequence(A.move_by(dx, dy, fragmentMovementSpeed, TIMING_EASE_IN), A.remove())) #actions2 = (A.sequence(A.fade_to(1.0, fragmentMovementSpeed, TIMING_EASE_IN), A.remove())) miniRocket.run_action(A.group((actions))) #, (actions2))) self.projectiles.append(miniRocket)
Here is my function that handles the creation of projectile objects (lasers, rockets, etc) that gets called if the user is touching the screen. (Depending on the upgrade level for the weapons fire rate, it may get called more often, I’ll post that code as well.)
def fireWeapon(self, yOffset=None, customXOffset=None): listOfColors = ['#F00', '#F0F', '#00F', '#FF0', '#0FF'] projectileCounter = 2 xOffset = 0 if yOffset != None: yOffset = yOffset else: yOffset = 10 if customXOffset != None: customXOffset = customXOffset else: customXOffset = 0 if self.activeWeapon == 'laser': self.numberOfProjectileLevel = self.laserNumberOfLasersLevel elif self.activeWeapon == 'rocket': self.numberOfProjectileLevel = 1 if len(self.listOfProjectiles) > 1: self.listOfProjectiles.clear() while self.numberOfProjectileLevel > len(self.listOfProjectiles): self.listOfProjectiles.append(f'fire{projectileCounter}') projectileCounter += 1 self.listOfProjectilesX.append(self.listOfXOffsets.pop(0)) xInProjectileCounter = len(self.listOfProjectiles) - 1 if self.activeWeapon == 'rocket': playSound = sound.play_effect('game:Woosh_1') actions = [A.move_by(0, self.size.height * 0.75, 1.25 * self.speed), playSound, A.remove()] #x = Rocket(parent=self) elif self.activeWeapon == 'laser': playSound = sound.play_effect('arcade:Laser_6') actions = [A.move_by(0, self.size.h/2 + (5*self.laserPowerLevel), 0.25 * self.speed), playSound, A.remove()] #x = SpriteNode('spc:LaserBlue8', parent=self, y_scale = 0.10, x_scale = 0.25) for x in self.listOfProjectiles: xOffset = self.listOfProjectilesX[xInProjectileCounter] if self.activeWeapon == 'rocket': x = Rocket(parent=self) elif self.activeWeapon == 'laser': x = Laser(parent=self) x.z_position = 1 x.collisionsLeft = self.laserPowerLevel #x = SpriteNode('spc:LaserBlue8', parent=self, y_scale = 0.10, x_scale = 0.25) """ if self.activeWeapon == 'rocket': playSound = sound.play_effect('game:Woosh_1') actions = [A.move_by(0, self.size.height / 2.0, 1.25 * self.speed), playSound, A.remove()] x = Rocket(parent=self) #x.scale = 0.025 """ """ if self.activeWeapon == 'rocket': playSound = sound.play_effect('game:Woosh_1') actions = [A.move_by(0, self.size.h, 1.50 * self.speed), playSound, A.remove()] x = Rocket(parent=self) x.scale = 0.025 """ """ if self.activeWeapon == 'laser': playSound = sound.play_effect('arcade:Laser_6') actions = [A.move_by(0, self.size.h/2, 0.75 * self.speed), playSound, A.remove()] x = SpriteNode('spc:LaserBlue8', parent=self, y_scale = 0.10, x_scale = 0.25) """ x.position = self.ship.position + (xOffset + customXOffset, yOffset) xInProjectileCounter -= 1 x.run_action(A.sequence(actions)) self.projectiles.append(x) #print(f'x.collisionsLeft: {x.collisionsLeft}') if self.numberOfProjectileLevel >= 5: x.color = listOfColors[0] if self.numberOfProjectileLevel >= 10: x.color = listOfColors[1] if self.numberOfProjectileLevel >= 15: x.color = listOfColors[2] if self.numberOfProjectileLevel >= 20: x.color = listOfColors[3] if self.numberOfProjectileLevel >= 25: x.color = listOfColors[4] if self.numberOfProjectileLevel >= 30: x.color = 'white'
Here is what gets called as long as the number of touches is >= 1.
This is what calls the function I pasted above.
def shouldFire(self): if self.game_over == False: if self.activeWeapon == 'laser': self.defaultWeaponTimer = self.laserFireRateLevel elif self.activeWeapon == 'rocket': self.defaultWeaponTimer = self.rocketFireRateLevel elif self.activeWeapon == 'ropeLaser': self.defaultWeaponTimer = self.ropeLaserFireRateLevel if self.defaultWeaponTimer < 20: self.maxSpeedLevel = 0 if self.defaultWeaponTimer >= 20 < 30: self.maxSpeedLevel = 1 #self.defaultWeaponTimer = 0 if self.defaultWeaponTimer >= 30 < 40: self.maxSpeedLevel = 2 #self.defaultWeaponTimer = 0 if self.defaultWeaponTimer >= 40 < 50: self.maxSpeedLevel = 3 #self.defaultWeaponTimer = 0 if self.defaultWeaponTimer >= 50 < 60: self.maxSpeedLevel = 4 if self.defaultWeaponTimer >= 60 < 70: self.maxSpeedLevel = 5 if self.defaultWeaponTimer >= 70 < 80: self.maxSpeedLevel = 6 if self.defaultWeaponTimer >= 80: self.maxSpeedLevel = 7 #print(f'self.defaultWeaponTimer: {self.defaultWeaponTimer}') #print(f'self.maxSpeedLevel: {self.maxSpeedLevel}') if self.weaponTimer >= 100: if self.maxSpeedLevel == 0: self.fireWeapon() elif self.maxSpeedLevel == 1: self.fireWeapon(20, -5) self.fireWeapon(20, 5) elif self.maxSpeedLevel == 2: self.fireWeapon(20, -5) self.fireWeapon(20, 5) self.fireWeapon() elif self.maxSpeedLevel == 3: self.fireWeapon(30, -10) self.fireWeapon(30, 10) self.fireWeapon(20, -5) self.fireWeapon(20, 5) elif self.maxSpeedLevel == 4: self.fireWeapon(30, -10) self.fireWeapon(30, 10) self.fireWeapon(20, -5) self.fireWeapon(20, 5) self.fireWeapon() elif self.maxSpeedLevel == 5: self.fireWeapon(40, -15) self.fireWeapon(40, 15) self.fireWeapon(30, -10) self.fireWeapon(30, 10) self.fireWeapon(20, -5) self.fireWeapon(20, 5) elif self.maxSpeedLevel == 6: self.fireWeapon(40, -15) self.fireWeapon(40, 15) self.fireWeapon(30, -10) self.fireWeapon(30, 10) self.fireWeapon(20, -5) self.fireWeapon(20, 5) self.fireWeapon() elif self.maxSpeedLevel == 7: self.fireWeapon(50, -20) self.fireWeapon(50, 20) self.fireWeapon(40, -15) self.fireWeapon(40, 15) self.fireWeapon(30, -10) self.fireWeapon(30, 10) self.fireWeapon(20, -5) self.fireWeapon(20, 5) self.weaponTimer = self.defaultWeaponTimer self.weaponTimer += 5
I tried to include as much code as possible, but I did not clean it up, so excuse the mess and the excessive block comments. I was trying to troubleshoot, etc.
Here is my question:
How should I go about reducing the stutter caused by creating my MiniRockets?
In general, is there a more efficient way to create many objects? In such a way that stuttering is minimal?
I create up to 50 or so of the MiniRocket objects depending on the upgrade level. But these are created >3 times per second in some cases. So 150+ objects created and tracked.
I reduced stuttering quite a bit by commenting out the MeteorSmall and MeteorTiny creations, which were originally set to create 3. The version above only creates 2 of the MeteorMedium objects to reduce stutter.
Any help is appreciated! I hope that including this much code helps.
Also, for people working on a similar game:
Feel free to reuse anything that may be useful! -
How do you create your projectiles? By a call to SpriteNode? Or do you cache the objects?
Maybe figure out what the max number of projectiles of each type can be on the screen at one time -- then create all of the sprites during setup. You would then have a list of onscreen and non-active rockets -- when you need to spawn 50 rockets, you pop 50 from the non active list, add to the scene, and append to the onscreen list. When each rocket is destroyed or goes off screen, you pop from the onscreen list, and add to the non-active list, ready to be reused. Just reset the position each time.
If you ever run out of non-active rockets you can create new ones and append to the list, but just never destroy any. -
@Robert_Tompkins said:
if isRocket == 1:
fragmentsToCreate = round(self.rocketFragmentsLevel)
fragmentMovementSpeed = 7.5 - (self.rocketFragmentsLevel * 0.1)
if fragmentMovementSpeed <= 1.0:
fragmentMovementSpeed = 1.0^^ This is where the MiniRockets are created currently. ^^
You can find the rest of the code above if you need it.
But ‘self.rocketFragmentsLevel’ currently has no limit, though I can cap it at 99 or 100.
The hard part will be calculating the max number on screen. ..unless I modify my save file to max out my levels, and add code to count the number on screen.. yea I’ll do that.I do create these outside of the Scene:
class Rocket (SpriteNode): def __init__(self, **kwargs): if path.exists('rocket.png'): img = 'rocket.png' SpriteNode.__init__(self, img, scale = 0.05, **kwargs) else: img = 'spc:PlayerLife3Blue' SpriteNode.__init__(self, img, scale = 0.15, **kwargs) class MiniRocket (SpriteNode): def __init__(self, **kwargs): img = 'plf:LaserPurpleDot' SpriteNode.__init__(self, img, scale = 0.05, **kwargs)
I will try out what you mentioned, that sounds like it should do the trick!
If that works well, I will do the same thing with the MeteorTiny creations as well.
Thanks, @JonB
This may take me a few days knowing me, but I’ll reply back with results! -
Alright, whaaaat am I not seeing here. Lol.
So I create a list in my new_game() instead of setup() but that’s only because I call new_game() to prevent the need to relaunch the program. Anyway.I create a list of (now) 1000 fragments, to ensure there are plenty available.
Everything went fine, fragments were being created, etc. until they stopped entirely.
It seemed to correlate with the number in the list. As in, I believe the fragments in the list are being used once, then are no longer being considered.
If I look at the list of inactiveFragments when they stop being created, it is full of MiniRocket objects.
I moved the creation(popping from self.listOfInactiveFragments and appending to self.projectiles) into the same function that handles removing the projectiles to make it easier to troubleshoot.I just can’t seem to find why they stop being created once I iterate through each from the initial list. Any ideas?
Here is where I create them. (Each time this is called, I can create len(self.listOfInactiveFragments) fragments before they ‘run out’.
def new_game(self): if self.laserFireRateLevel > 59: self.ship = SpriteNode('spc:PlayerShip3Red') elif self.laserFireRateLevel > 29: self.ship = SpriteNode('spc:PlayerShip2Red') else: self.ship = SpriteNode('spc:PlayerShip1Red') self.ship.scale = 0.50 self.ship.position = self.size / 2 self.add_child(self.ship) self.ship.position = (self.size.w/2, 32) self.stageTimer = 600 self.stageLabel.text = f'Stage: {self.stageNumber}' self.score_label.text = self.siConvert(self.score) print(f'self.ship.bbox: {self.ship.bbox}') self.listOfInactiveFragments = [] self.listOfActiveFragments = [] # I ENDED UP USING self.projectiles INSTEAD OF THIS for x in range(1000): x = MiniRocket(parent=self) self.listOfInactiveFragments.append(x) #print(f'self.listOfInactiveFragments: {self.listOfInactiveFragments}') self.showPlayMenu() if profileLoaded == True: for item in self.items: item.remove_from_parent() self.items = [] self.projectiles = [] self.upgradeCostMultiplier = 2.0
Here is where the projectiles are created (near the end). I added handling to prevent them from being tossed out, I thought.
def check_laser_collisions(self): for projectile in list(self.projectiles): if not projectile.parent: #print(projectile) if type(projectile) == MiniRocket: #print('IT HAPPENED') indexOfProjectile = self.projectiles.index(projectile) self.listOfInactiveFragments.append(self.projectiles.pop(indexOfProjectile)) else: self.projectiles.remove(projectile) continue for item in self.items: if not isinstance(item, Meteor): if not isinstance(item, Meteor2): if not isinstance(item, Meteor3): if not isinstance(item, Meteor4): if not isinstance(item, Meteor5): if not isinstance(item, Meteor6): if not isinstance(item, MeteorMedium): if not isinstance(item, MeteorSmall): if not isinstance(item, MeteorTiny): continue if item.destroyed: #print("Entered if item.destroyed:") continue #print("Reached if projectile.position in item.frame:") if projectile.position in item.frame: if isinstance(projectile, Rocket): # sound.play_effect('arcade:Explosion_2') self.destroy_meteor(item, 1) #self.projectiles.remove(projectile) #projectile.remove_from_parent() fragmentsToCreate = round(self.rocketFragmentsLevel) fragmentMovementSpeed = 7.5 - (self.rocketFragmentsLevel * 0.1) if fragmentMovementSpeed <= 1.0: fragmentMovementSpeed = 1.0 #for i in range(fragmentsToCreate): for i in range(fragmentsToCreate): miniRocket = self.listOfInactiveFragments.pop(0) miniRocket.collisionsLeft = self.rocketFragmentPenetrationLevel miniRocket.scale = 0.25 miniRocket.color = '#07ff1c' miniRocket.z_position = 0 miniRocket.position = projectile.position fullCircle = random.uniform(0, pi*2) #upwardAngle = random.uniform(0, pi) dx, dy = cos(fullCircle) * 250, sin(fullCircle) * 250 #d = 1.25 #miniRocket.rotation = fullCircle actions = (A.sequence(A.move_by(dx, dy, fragmentMovementSpeed, TIMING_EASE_IN), A.remove())) #actions2 = (A.sequence(A.fade_to(1.0, fragmentMovementSpeed, TIMING_EASE_IN), A.remove())) miniRocket.run_action(A.group((actions))) #, (actions2))) self.projectiles.append(miniRocket) self.projectiles.remove(projectile) projectile.remove_from_parent() #m = SpriteNode(explosionTexture, scale = 0.50, color = '#ffaf57', parent=self) #m.position = projectile.position #m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) if isinstance(projectile, MiniRocket): #sound.play_effect('arcade:Explosion_2') self.destroy_meteor(item) projectile.collisionsLeft -= 1 if projectile.collisionsLeft <= 0: indexOfProjectile = self.projectiles.index(projectile) self.listOfInactiveFragments.append(self.projectiles.pop(indexOfProjectile)) #self.projectiles.remove(projectile) #projectile.remove_from_parent() #self.listOfInactiveFragments.append(self.listOfActiveFragments.pop(0)) #m = SpriteNode(explosionTexture, scale = 0.25, color = '#ffaf57', parent=self) #m.position = projectile.position #m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) if isinstance(projectile, Laser): self.destroy_meteor(item) self.destroy_meteor(item) projectile.collisionsLeft -= 2 if projectile.collisionsLeft <= 0: self.projectiles.remove(projectile) projectile.remove_from_parent() else: self.destroy_meteor(item) #self.projectiles.remove(projectile) #projectile.remove_from_parent() break
I added a couple print()’s That are called when my ship hits a meteor, and I did this when the fragments stopped appearing. Here is the code I used for it, and the printed results:
if resetFlag != None: self.showStartMenu() else: print(f'self.listOfInactiveFragments: {self.listOfInactiveFragments}') for x in self.projectiles: if type(x) == MiniRocket: self.listOfActiveFragments.append(x) print(f'self.listOfActiveFragments: {self.listOfActiveFragments}') print(f'len(self.listOfActiveFragments): {len(self.listOfActiveFragments)}') print(f'len(self.listOfInactiveFragments): {len(self.listOfInactiveFragments)}') self.saveProfile(self.playerName) #print(f'self.projectiles: {self.projectiles}') self.showGameOverMenu()
<many MiniRocket objects here>
<main.MiniRocket object at 0x113075eb8>, <main.MiniRocket object at 0x113075e08>, <main.MiniRocket object at 0x113075f10>]
self.listOfActiveFragments: []
len(self.listOfActiveFragments): 0
len(self.listOfInactiveFragments): 1000 -
Alright, I assume this problem has something to do with the fact that when the object is no longer in view, it no longer is a child of the scene.
This parent-less object is then added to the list, for me to reuse. But since it is no longer a child of the scene, it is not presented??
If that’s the case, how do I tell the object that it needs to remember it’s parent, even if it is out of sight?
When I noticed that the objects parent = None, I tried telling it that it’s parent was self, but I get:
AttributeError: attribute 'parent' of '_scene2.Node' objects is not writable.
Maybe add_child(node)? I’ll prolly call it a night lol. -
O.
self.add_child(projectile) did the trick.
Thanks for the help @JonB
I will stress-test it tomorrow on my lunch.
Well, just found out that the number of fragments on-screen can easily be > 1000.
The performance seems much better already, I just need to make sure the fragments leave the scene’s view, otherwise they just sort of sit there for a while haha. -
Alright, I tried. It isn’t terrible until you start ramping things up.
Is there a way for me to copy/paste or upload my code (it’s > 32000 chars) so that it can be reviewed as a whole?I am still experiencing huge slow-down. Again, this might just be a limitation of Python and my iPhone pro max, but I am not sure. But I can upload my code if there’s somewhere I can do that.
-
@Robert_Tompkins, some git repository like Github is a popular choice. With a client app like Working Copy you can get nice version control flow for Pythonista.
-
From the wrench menu, share, share to Gist -- this will post a gist, then copies the link to the clipboard.
-
Incidentally, you might try using collections.deque instead of list -- it is designed for this sort of thing and is probably a little faster and more memory friendly (doesn't reallocate)
-
Great, thanks. I will try swapping out my lists later today or tomorrow.
I might also need to play around more with the whole ‘parent’ side of things, since I feel like that keeps hanging me up. I could benefit a ton by understanding it fully.
I’ll paste the instructions I typed out yesterday before I found out I couldn’t upload it here lol.If you have an iPhone 11 pro max, or Can resize the scene for your device easily.. feel free to try it out and experience it first hand.
If you tap Load Profile and enter ‘ADMIN’ or ‘SUDO’, you can specify a stage number and start with enough cash to upgrade either weapon plenty.I removed most of the sounds and most of the explosion textures, etc to narrow down the cause.
I also commented out all the unused functions/code to make it easier to navigate.
But it isn’t perfect haha. I typically clean my code up once I am happy with the functionality, which I haven’t achieved yet.Below you will find the program, as well as a modified game_menu.py that I modified to allow me to resize the menu by passing in additional arguments. I believe I only needed this functionality when I was recording and displaying a leaderboard. Anyway.
Make sure you name the game_menu as follows:
GameMenu.py
If you have issues running it, let me know. I think I added handling in there to take care of any custom-made items that others may not have on their device but could have missed something.
And no guarantees it will work/compile at all!
But it does work on my iPhone 11 Pro Max iOS 13.0 with Pythonista version 3.3 (latest version on Apple App Store).Here is the Main Code
https://gist.github.com/dad61ed3b8a5a83cd72f4acc5932db42Here is the modified menu code
https://gist.github.com/56ee84710967319b7bc4e8904d9391cf -
@Robert_Tompkins Im playing around with it but notice a few things:
It will probably be a little better to predefine your Texture's at the start, then pass the Textures to your SpriteNodes, rather than an image name. That saves a few millisec per call, on my old iPad anyway, but probably more important, I think it saves memory, though I can't prove it.
In other words, outside of init:
MeteorBrown=Texture('spc:MeteorBrown')
Then inside of MeteorBrown init
SpriteNode.init(MeteorBrown,...)I notice that you are still adding/destroying some meteor classes, and maybe doing add_child earlier than you intend.
I can't help but think that there might be a cleaner super class of your various Meteors, such that you don't need separate logic for each type, and can maybe just have a meteor factory that takes an integer/enumeration.
It would be useful to add some logging calls to see which methods called during update are taking the most time -- is it spawning? Collision detection? Something else? One could imagine that if you are doing collision detection on N projectiles against M meteors, that is going to add up quickly -- essentially O(N*M). So one key to speed is going to be to figure out a way
to get down to more linear performance -- for instance if rockets only travel along vertical paths, there may be a way to group projectiles based on x position when they are created based on the range of x that they will see over the screen... then that might be a faster downselect of which projectiles you need to consider for a given meteor .
(Or, some sort of kdtree type approach to finding nearest neighbor, and only checking the nearest neighbor projectiles to each meteor -- kdtrees can be a lot faster than computing all pairwise distances, and there are some pure python implementations, or perhaps aa numpy implementation). -
@JonB Awesome, thanks for taking the time to play around with it!
I will look into what you mentioned about the meteor init and try it out.I did notice an improvement when I added the fragments into a list.
But the moment I did the same for the meteors, I noticed that things start slowing down a ton, even without a bunch of fragments on screen.
For example, using the laser weapon, I still see the ‘slow-mo’ movement. I also disabled all sound except the weapon firing to help me listen for changes in performance. I can hear it bogging down for sure.I did add some logging to determine the time difference between creating a huge list and then popping everything from the list with the variable being using a ‘deque’ and using a standard list. Did the same with a dictionary. Funny enough, list seems faster. I tried to use it the way it was intended (fast access/modification from either end of list) but still saw the list out-performing.
I am in the process of simplifying my meteor creation and removing unnecessary calls, arguments, etc. so once I have it barebones, I will look at it from a ‘meteor generator’ perspective.
The other idea I had was similar to Chicken Invaders. Where instead of meteors, it was a set amount of enemies on screen, that just moved back and forth slightly, dropping danger until they are destroyed. Then a new stage is presented, etc.. This would limit the number of ‘items’ on screen, or at least define the exact number being presented. But this would require a lot more work, and I would want to clean everything up before I do that anyway.
Thanks for the ideas and feedback! I will add some more code to figure out what might be eating up resources. I did try multi-threading on each collision method, this may have improved performance slightly, but had some ‘pop from empty list’ errors, understandably.
-
This is NOT inside update(), it is in setup. Just for reference.
Time to add 5000 MiniRockets List: 0.2149660587310791These are all inside of update.
Max Item Collision execution time: 0.001150369644165039
Max Laser Collision execution time: 0.026622772216796875
Max Spawn Item execution time: 0.0004971027374267578
Max Should Fire execution time: 0.0007219314575195312The max value is a global variable and is only overwritten if the current call is > Max.
It looks like Laser Collisions are the worst, with Item Collisions coming in second.
Let me know if you have any recommendations on improving the laser collision checks. Aside from what you already recommended, of course.Here is how I’m recording it:
def update(self): global maxExecutionTimeItem global maxExecutionTimeLaserCollision global maxExecutionTimeSpawnItem global maxExecutionTimeShouldFire #if self.game_over: #return startTimeItem = time() self.check_item_collisions() stopTimeItem = time() executionTimeItem = stopTimeItem - startTimeItem if executionTimeItem > maxExecutionTimeItem: maxExecutionTimeItem = executionTimeItem #print(f'Item Collision execution time: {executionTimeItem}') startTimeLaser = time() self.check_laser_collisions() stopTimeLaser = time() executionTimeLaser = stopTimeLaser - startTimeLaser if executionTimeLaser > maxExecutionTimeLaserCollision: maxExecutionTimeLaserCollision = executionTimeLaser #print(f'Laser Collision execution time: {executionTimeLaser}') startTimeSpawnItem = time() if random.random() < 0.05 * self.stageNumber: if len(self.items) < 100: self.spawn_item() stopTimeSpawnItem = time() executionTimeSpawnItem = stopTimeSpawnItem - startTimeSpawnItem if executionTimeSpawnItem > maxExecutionTimeSpawnItem: maxExecutionTimeSpawnItem = executionTimeSpawnItem #print(f'Spawn Item execution time: {executionTimeSpawnItem}') if (len(self.touches)) >= 1: startTimeShouldFire = time() self.shouldFire() stopTimeShouldFire = time() executionTimeShouldFire = stopTimeShouldFire - startTimeShouldFire if executionTimeShouldFire > maxExecutionTimeShouldFire: maxExecutionTimeShouldFire = executionTimeShouldFire #print(f'Should Fire execution time: {executionTimeShouldFire}')
-
@Robert_Tompkins, I gave the game a try on an iPhone 11 Pro. It looked good, and I noticed no performance issues.
I did not get very far in the game, though, mainly due to some playability kinks:
- Ship jumping to finger location. instead of moving relative to finger movement, killed me several times.
- Game tended to go to the pause screen very easily, which broke the flow.
- Game balance seemed off, first a few little rocks, then a screenful.
-
@mikael said:
@Robert_Tompkins, I gave the game a try on an iPhone 11 Pro. It looked good, and I noticed no performance issues.
I did not get very far in the game, though, mainly due to some playability kinks:
- Ship jumping to finger location. instead of moving relative to finger movement, killed me several times.
- Game tended to go to the pause screen very easily, which broke the flow.
- Game balance seemed off, first a few little rocks, then a screenful.
Hey thanks for trying it out, I wonder why I keep running into performance issues then.
The jumping shouldn’t happen at all!
Pausing should only happen when your finger lifts off the screen.
I added that ‘feature’ because the upgrading required you to pause the game (top left corner) and by the time you came back out of it, the meteors would be on you before you had time to react lol.Yes, balance is way off in that version haha, that’s my ‘dev’ version. I tweaked it so that there was only 1 main currency, and it awarded a HUGE amount, and dropped often. This way I could upgrade a ton and stress it. But regardless, thanks for the feedback!
I also tweaked the stages from 30 seconds down to 10. So that may explain the none then flood.
-
Just an update, yes, I still play with this off/on. Minor tweaks, adding ideas, etc..
Anyway. I managed to improve the performance of the game by doing the following.I found that ‘generating’ lists of meteors(parent=self) as well as projectiles(parent=self) at the start of the game helps. For example, 500 basic meteors.. 2000 laserProjectiles.
Each time a new meteor is to be spawned, I pop it from the list and move on.
Each time a projectile is to be spawned, I pop it and move on as well.The Stage Timer is happy at 15 seconds (60 second Boss Round).
Each time the Stage Timer hits 0, the Stage Number increments. When this happens, if Stage Number % 10 != 0, I check the length of each list. If any list is less than a threshold, I enter a function that generates objects.
If any list count is VERY low, I append 1000/4000 meteors/projectiles to their respective list.
Otherwise, I generate less of each, just enough to double the threshold. This prevents frame drops due to excess generation mid-stage and small ‘loading’ times between stages is acceptable. Like 100ms loading times lol.I found that generating initial lists that are >10000 or totaling ~20k children, the frames drop even if none of those children are in-view. I assume memory issues. So I try to keep the lists just large enough to sustain them to the next Stage.
So to put it simply... Create lists at startup with items, pop from the lists as needed, and refill the lists as needed between stages. In emergencies mid-stage, I am able to call the generation function as well, to prevent popping from an empty list.
Anyway, thanks again for everybody’s help with this!
Oops, this appears to be what @JonB was recommending all that time ago..
Thanks Jon, now I get it ;) -
Another way to avoid the pain of meteor creation is to reuse the destroyed meteors (or meteors far outside of the game area)
Just like you pop a meteor when you want to use it, push it back (maybe better to use a queue -- pop_right and push_left). You can change the size when you push it back, and reinitialize the position, speed, etc. That way you only ever need queues as long as the number that can be onscreen at one time -- rather than creating 1000 at a time, I have to think that 1000 is more than enough if you reuse them.
Note you can also change the texture of a sprite (with a stored version of a Texture), and that's got to be faster than creating a whole new object.