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
-
Hi all, have been loving Pythonista for a few years now as a way to develop my programming skills.
I recently transitioned from building UI’s for my circuit calculating programs into building games using the Scene module.I used OMZ’s little Alien shooter tutorial as a template to build a classic ‘space shooter’ game.
Here’s the question:
Is there a way (I know there is) to use my own imported image as the scene background?I’d like to replace the current background:
self.background_color = ('#000000')
With a more fitting background/scene.
Essentially, I’d like to do this:self.background_color = ('deepSpace.png')
But when that didn’t work (understandably), I tried:
self.background = ('deepSpace.png')
Etc.
I guess I could create a new SpriteNode that extends across the x,y of my scene.. but wanted to be sure there wasn’t a better way to do this.
Thanks,
RHT -
So I ended up just rolling with this:
class MyScene (Scene): def setup(self): self.background = SpriteNode('deepSpace.png', size = self.size, position = self.size / 2) self.add_child(self.background)
Now I just need to figure out how to get a higher quality image loaded in.
It seems that larger files may take too long to load, resulting in failure to load texture.
Or, if somebody has an example I could reference or tips, I would like to animate the background to give the impression of ‘life’.
Maybe some twinkles, slight movement, etc.
I haven’t found tutorials or good examples covering the use of shaders or textures to do this.
Otherwise, I’ll consider this mission accomplished ..for now. -
Not sure what you’re stuck on but if you want to animate your background to give the impression of life what you really want to do is not animate the background itself but the children sprites that you will add into your background node.
So if you want to twinkle little stars in your background, you will create your star spriteNode and add it as a child of your background node and run an action :move,rotate,scale etc.
if you really want to animate the background and not objects that are on it then it’s all the same since background it’s a spritenode.
About the image quality i don’t think it should be a problem to load a big image, it’s just one image.
-
Yes, that will work great! Thanks for that idea.
I’m starting to get the hang of using actions.
Just had an aha moment earlier with them when I was trying to figure out a way to produce 3 new, smaller meteors originating from 1 destroyed meteor. Then I went all crazy resulting in 3^4 objects created from one in some cases.Anyway, yes, I will add some sprites to my background, maybe I’ll have the stars fadeIn/fadeOut depending on update(). Thanks!
Regarding the image problem:
I feel like I ruled out all other variables when I was testing it out. The original size of my image was ~4MB. I ended up taking a screenshot of it, importing it locally, passing the new IMG name, got the ‘failed to load texture’ error.
So I rotated the image and took another screenshot of it, cropped it, resulting in an image < 1MB.
Only then was I able to successfully load the texture.
I’ll give it another shot, but I do remember seeing some function in the docs something like ‘load_texture’ meant to improve performance if used prior to presenting it. If I can find it, I’ll try that too.Again, thank you for the suggestions!
-
Found this after an hour of digging:
@JonB said:
ohh, ok. scene Textures are opengl based, i think. So you cannot have a Texture that is larger in pixels that the graphics memory region, which is something like twice largest screen size (for retina displays).
with ui.ImageContext(1024*2,1024*2) as ctx: #ui.Image.named('IMG_0625.JPG').draw() Texture(ctx.get_image())
The above works, on my Ipad3. Anything up to 2048 in either row/col works, but even 2049x1 fails. so, that tells me the opengl textures must be <=2048 in either dimension.
I’m currently using an iPhone 11 Pro Max.
I went ahead and used his test method, implemented outside of and prior to any Scene related activity.with ui.ImageContext(1365,1365) as ctx: ## ANYTHING > 1365 FOR EITHER ARGUMENT INVOKES ('ValueError': Could not load image) ON LINE 58: ‘background = SpriteNode(largeImage)‘ ui.Image.named('hqDeepSpace.png').draw() largeImage = Texture(ctx.get_image()) background = SpriteNode(largeImage)
hqDeepSpace.png specs:
PNG Image
2.6MB
6000x4000 pxMy knowledge level in this area is minimal, so if somebody else is better equipped to solve this or find a work around, etc. let me know.
Otherwise, I will stick with my lower quality image. -
@Robert_Tompkins you could set the Scene background as transparent and add an ImageView as subview, like in this topic
Something like
from objc_util import * from scene import * import ui glClearColor = c.glClearColor glClearColor.restype = None glClearColor.argtypes = [c_float, c_float, c_float, c_float] glClear = c.glClear glClear.restype = None glClear.argtypes = [c_uint] GL_COLOR_BUFFER_BIT = 0x00004000 class ChristmasScene(Scene): def setup(self): objv = ObjCInstance(self.view) objv.glkView().setOpaque_(False) sp = SpriteNode('emj:Christmas_Tree', anchor_point=(0,0), position=(500,300), parent=self) def draw(self): glClearColor(0, 0, 0, 0) glClear(GL_COLOR_BUFFER_BIT) w, h = ui.get_window_size() v = ui.ImageView(frame=(0,0,w,h)) v.image = ui.Image.named('test:Peppers') gameview = SceneView() gameview.scene = ChristmasScene() gameview.add_subview(v) v.send_to_back() gameview.present('full_screen')
-
Here is my star method if you want.
def instantiate_star(self): self.star = SpriteNode('white_circle_2.png') self.star.z_position = random.choice([-1,0,2]) self.star.alpha=0 self.star.scale =0.01/2 self.star.position = (random.uniform(0,self.size.w), random.uniform(0,self.size.h)) self.star.rotation= (random.uniform(0,360)) d = random.uniform(0.5, 1) i = random.uniform(3,6) j = random.uniform(2,5) actions = [A.scale_by(random.uniform(0,0.01), i), A.fade_to(d,i), A.scale_to(random.uniform(0.02,0.0), j),A.fade_to(0,j),A.remove()] self.star.run_action(A.sequence(actions)) self.starNode.add_child(self.star)
You’ll just have to replace the sprite with some white circle image
-
@cvp
Transparent? Again, another good idea that never crossed my mind. Even better, implementing the UI Module.
I started with UI, definitely prefer it over Tk. I have yet to (knowingly) blend UI, Scene objects into one program. If it works well, that will open a lot of doors and I’ll be able to reuse a ton of my old code. Thanks for the example!@mwsx
Sweet, code snippets are the best learning tool for me. I’ll throw that in my code and tweak some stuff a bit. Thanks for the help!Now all that’s left is for me to find a way to better handle the stuttering I’ve been experiencing when my game starts creating the ‘clusters’ of meteors.. Most likely this is just a limitation of developing a game like this via Python-mobile.
But the stuttering occurs when I shoot a meteor falling on the screen. This meteor has a chance to break into 3 smaller meteors, each of these has a chance to break into 3 more, and those 3 will do the same. For each of the smallest meteors broken, a star is created, that slowly falls along the x-axis.
Each star I had flashing between 2 colors to keep it from blending in with harmful debris, but removed that to improve performance. All of those objects can be created and destroyed in a matter of 200-500ms.
I’ve had to use async-wrappers for some of my UI programs, is this something that can be utilized here? Or will I just need to slow the baby making down a bit? -
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!