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
-
I have way more things acting in my scene and everything’s running fine, everytime I saw a dropped in FPS it was becauseI did something wrong not because of any limitations. Maybe you're instantiating things in a loop or maybe you just have a print in update which makes the FPS drop pretty fast also.
Also do you remove the meteors from their parent once they get destroyed ?
If you could paste the meteor code I could check what’s the problem -
I do have a few things printing, though I make sure I don’t throw them in update() unless I toss a counter in there limiting it a bit.
Btw, that star node worked great! It goes perfect with my background. I had to tweak it a bit to reduce the number on-screen at a time because according to my wife, it’s “too distracting”.Anyway, I’ll post the meteor section. Unfortunately, I created quite a few sprites from custom images I made in paint. So if you want the full code to experiment more, I’d have to give you a handful of crudely drawn images as well haha.
Also, I am just now realizing i reaaallyy need to clean this section up. I went ahead and added notes to it to reduce confusion!
Sorry about the mess :D############################################################ ## This gets called when a laser intersects with a non-destroyed Meteor def destroy_meteor(self, meteor): #print(f'Entered destroy_meteor() with meteor: {meteor}') ######################################################## ## If meteor object is a Meteor: ###### Play generic explosion sound ###### Flag THIS meteor as destroyed ###### Swap texture of meteor with a coin ########## Creates 5 Sprites that disperse around meteor ########## Essentially gives appearance of it shattering if isinstance(meteor, Meteor): sound.play_effect('arcade:Explosion_2', 0.1) #print(f'isinstance(meteor, Meteor): item: {meteor}') meteor.destroyed = True meteor.texture = Texture('plf:Item_CoinBronze') #meteor.color = 'mediumspringgreen' for i in range(5): m = SpriteNode('spc:MeteorBrownMed1', parent=self) m.position = meteor.position + (random.uniform(-20, 20), random.uniform(-20, 20)) angle = random.uniform(0, pi*2) dx, dy = cos(angle) * 80, sin(angle) * 80 m.run_action(A.move_by(dx, dy, 0.6, TIMING_EASE_OUT)) m.run_action(A.sequence(A.scale_to(0, 0.6), A.remove())) ######################################################## ## If meteor object is a Meteor2: ###### Play generic explosion sound ###### 25% * stageNumber (stageNumber is an int between 1 and 20, so far) ########## Create 3 new meteors (meteorMedium) ########## Place them slightly above previous meteor to make it easier ########## Disperse downward at a slower speed ###### Create a special explosion visual to differentiate it from other meteors if isinstance(meteor, Meteor2): sound.play_effect('arcade:Explosion_2', 0.25) if random.random() < 0.25 * self.stageNumber: for i in range(3): meteorMedium = MeteorMedium(parent=self) meteorMedium.destroyed = False 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('shp:Explosion00', 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() ######################################################## ## Same as Meteor2 above with slight tweaks ## Creates 3 new meteors (meteorSmall) if isinstance(meteor, MeteorMedium): sound.play_effect('arcade:Explosion_2', 0.5) if random.random() < 0.20 * self.stageNumber: for i in range(3): meteorSmall = MeteorSmall(parent=self) meteorSmall.destroyed = False meteorSmall.position = meteor.position + (random.uniform(-50, 50), (-20 + random.uniform(-5, 5))) 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) m = SpriteNode('shp:BlackSmoke00', 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() ######################################################## ## Same as MeteorMedium above with slight tweaks ## Creates 3 new meteors (meteorTiny) if isinstance(meteor, MeteorSmall): sound.play_effect('arcade:Explosion_2', 0.75) if random.random() < 0.15 * self.stageNumber: for i in range(3): meteorTiny = MeteorTiny(parent=self) meteorTiny.destroyed = False meteorTiny.position = meteor.position + (random.uniform(-100, 100), random.uniform(-5, 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('shp:BlackSmoke00', 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() ######################################################## ## If meteor object is a MeteorTiny: ###### Play generic explosion sound ###### Flag THIS tiny meteor as destroyed ###### Swap texture of small meteor with a silver star ###### Drop star slowly along X axis if isinstance(meteor, MeteorTiny): sound.play_effect('arcade:Explosion_6', 1.0) meteor.destroyed = True meteor.texture = Texture('spc:StarSilver', scale = 0.10) #meteor.color = '#FA1BCA' d = random.uniform(5.0, 8.0) actions = [A.move_by(0, -(self.size.h + 30), d), A.remove()] meteor.run_action(A.sequence(actions)) ```
-
Aaa and here’s a copy without the comments.
Should be less confusing for the syntax highlighting algorithm.def destroy_meteor(self, meteor): #print(f'Entered destroy_meteor() with meteor: {meteor}') if isinstance(meteor, Meteor): sound.play_effect('arcade:Explosion_2', 0.1) #print(f'isinstance(meteor, Meteor): item: {meteor}') meteor.destroyed = True meteor.texture = Texture('plf:Item_CoinBronze') #meteor.color = 'mediumspringgreen' for i in range(5): m = SpriteNode('spc:MeteorBrownMed1', parent=self) m.position = meteor.position + (random.uniform(-20, 20), random.uniform(-20, 20)) angle = random.uniform(0, pi*2) dx, dy = cos(angle) * 80, sin(angle) * 80 m.run_action(A.move_by(dx, dy, 0.6, TIMING_EASE_OUT)) m.run_action(A.sequence(A.scale_to(0, 0.6), A.remove())) if isinstance(meteor, Meteor2): sound.play_effect('arcade:Explosion_2', 0.25) if random.random() < 0.25 * self.stageNumber: for i in range(3): meteorMedium = MeteorMedium(parent=self) meteorMedium.destroyed = False 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('shp:Explosion00', 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, MeteorMedium): sound.play_effect('arcade:Explosion_2', 0.5) if random.random() < 0.20 * self.stageNumber: for i in range(3): meteorSmall = MeteorSmall(parent=self) meteorSmall.destroyed = False meteorSmall.position = meteor.position + (random.uniform(-50, 50), (-20 + random.uniform(-5, 5))) 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) m = SpriteNode('shp:BlackSmoke00', 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() if isinstance(meteor, MeteorSmall): sound.play_effect('arcade:Explosion_2', 0.75) if random.random() < 0.15 * self.stageNumber: for i in range(3): meteorTiny = MeteorTiny(parent=self) meteorTiny.destroyed = False meteorTiny.position = meteor.position + (random.uniform(-100, 100), random.uniform(-5, 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('shp:BlackSmoke00', 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() if isinstance(meteor, MeteorTiny): sound.play_effect('arcade:Explosion_6', 1.0) meteor.destroyed = True meteor.texture = Texture('spc:StarSilver', scale = 0.10) #meteor.color = '#FA1BCA' d = random.uniform(5.0, 8.0) actions = [A.move_by(0, -(self.size.h + 30), d), A.remove()] meteor.run_action(A.sequence(actions))
-
I am not 100 percent sure, but I think you may end up with better performance if you
create all of your textures at the start, as scene.Texture's, then instantiate your sprite nodes with those. I just am not sure whether SpriteNode is smart enough to figure out that the Image with the same name is the same image and this only create one UI.Image, and one scene.Texture-- probably not. So every time you create a new SpriteNode, there is a fair amount of work creating new textures.It is probably best to create and save the textures first, up front, as named globals that can just be reused. SpriteNode accepts a Texture object.
Even better might be to create a set of SpriteNodes, that you reuse at different scale after they are destroyed.
Finally, for a large tiled object, there may be some performance benefit in using an EffectNode -- but I'm not sure.
Finally, check out @mikael's SpriteKit wrapper. This allows you to make use of physics, lighting, particle emitters, and so on.
https://forum.omz-software.com/topic/5802/spritekit-wrapper/18 -
@JonB
Thanks for the tips! I went ahead and defined some global textures and pass those in instead. Performance didn’t get much better, so I’ll try the reusing at smaller scale approach.
I also tried increasing the Δy for the stars to help get them off-screen ASAP. This didn’t help much either.
This tells me the main contributor is likely the creation/destruction of a large amount of entities.
It doesn’t help that I usually have my ‘laser’ upgraded to the point that I am creating and firing (+Δy) ~40-50 projectiles ~5x/s.
So 200+ projectiles shot in a +Δy destroying many entities falling in -Δy that create as many new star entities falling in a different -Δy.Thanks for that link! I’ll have to play with that after work. Looks like you contributed a fair bit to that as well.
If all fails, I’ll go through and clean up my code a bit. There’s plenty of room for improvement in that area. -
@Robert_Tompkins, sorry if I missed it, but did you try tiling the background, i.e. use a few composable images to build the background? There are suitable images on the internet, and this is efficient as the textures are only stored once.
-
@mikael
No I have not tried that yet.
I currently use a dark image of space with stars as my background(107KB) and add life to it using a heavily modified/toned down version of the method provided by @mwsx :def createDestroyStar(self): self.star = SpriteNode('shp:nova') #self.star.color = random.choice(self.listOfStarColors) if random.random() <= 0.25: self.star.color = '#ffefb3' else: self.star.color = '#FFFFFF' self.star.alpha = 0 self.star.scale = 0.25/5 self.star.position = (random.uniform(0,self.size.w), random.uniform(0,self.size.h)) self.star.rotation = (random.uniform(0,360)) noneToOne = random.uniform(0.0, 1) i = random.uniform(0,3) actions = [ A.scale_by(random.uniform(0, 0.01), i), A.fade_to(noneToOne, i), A.scale_to(random.uniform(0.02, 0.0), noneToOne), A.fade_to(0, noneToOne), A.remove() ] self.star.run_action(A.sequence(actions)) self.add_child(self.star)
I’m not sure how I would go about replicating this effect using multiple images. However, if you think this method/function is resource heavy, I would settle with just the image background.
I did a few things to improve performance:
- Scaled down effects to smaller size
- Replaced the ‘falling’ animation for my stars with a 100ms animation that pulls each star into the ship, removing it from the scene.
Overall, the performance is better than it has been. Considering the number of Sprites being generated and being destroyed in such a short amount of time, I think the stutter is reasonable.
-
@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