Forum Archive

Imported image as Scene background

Robert_Tompkins

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

Robert_Tompkins

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.

mwsx

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.

Robert_Tompkins

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!

Robert_Tompkins

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 px

My 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.

cvp

@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') 
mwsx

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

Robert_Tompkins

@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?

mwsx

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

Robert_Tompkins

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)) ```
Robert_Tompkins

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))
JonB

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

Robert_Tompkins

@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.

mikael

@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.

Robert_Tompkins

@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.

mikael

@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.

Robert_Tompkins

@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!

Robert_Tompkins

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!

JonB

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

@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!

Robert_Tompkins

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() 


<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

Robert_Tompkins

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.

Robert_Tompkins

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.

Robert_Tompkins

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.

mikael

@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.

JonB

From the wrench menu, share, share to Gist -- this will post a gist, then copies the link to the clipboard.

JonB

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)

Robert_Tompkins

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/dad61ed3b8a5a83cd72f4acc5932db42

Here is the modified menu code
https://gist.github.com/56ee84710967319b7bc4e8904d9391cf

JonB

@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).

Robert_Tompkins

@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.

Robert_Tompkins

This is NOT inside update(), it is in setup. Just for reference.
Time to add 5000 MiniRockets List: 0.2149660587310791

These 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.0007219314575195312

The 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}') 
mikael

@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.
Robert_Tompkins

@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.

Robert_Tompkins

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 ;)

JonB

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.

Robert_Tompkins

@JonB said:

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.


So about that.. I do agree 100% and I think you recommended this as well. However, I attempted this and my implementation required a ton of additional handling for popping the specific object. (I did this for all objects, but all behaved similarly so we will focus on one meteor type!)

The way I set it up was using 2 lists: listOfInactiveMeteors, listOfActiveMeteors
When a meteor is spawned, I would pop from inactive, append to active, run actions.

If an activeProjectile intersected with an activeMeteor, the ‘health’ of both objects (projectile/meteor) would decrement by 1.
If projectileHealth <= 0: pop projectile from active, append to inactive.
Found some code I never fully removed showing at least some of this implementation on a meteor:
elif isinstance(meteor, Meteor2): if meteor in self.items: indexOfMeteor = self.items.index(meteor) if activeWeapon == 'laser': meteor.livesRemaining -= 9 meteor.livesRemaining -= 1 if meteor.livesRemaining <= 0: meteorPosition = meteor.position #indexOfMeteor = self.items.index(meteor) if meteor in self.activeMeteors: m = SpriteNode(explosionTexture, scale = 0.25, parent=self) m.position = meteorPosition m.run_action(A.sequence(A.fade_to(0, 0.3, TIMING_EASE_OUT), A.remove())) sound.play_effect('arcade:Explosion_3', 0.50) meteor.destroyed = True meteor.remove_from_parent() self.activeMeteors.remove(meteor) #self.listOfInactiveMeteor2.append(self.items.pop(indexOfMeteor)) #meteor.remove_from_parent() self.collect_item("money", (1E6 * (self.stageNumber**3)))

Robert_Tompkins

So with that implementation, I believe I had issues with popping that specific meteor from the list because by the time I executed the line that popped it from active and appended it to inactive, it no longer existed. Then I had additional trouble with the meteor no longer being a child when I would attempt to ‘reactivate’ a meteor I had Previously popped from active and appended to inactive. Is there a way to remove an object from view without the parent ‘disowning’ it? Because I just sort of disown all children to get them out of sight.

I believe the main issues I had were due to not fully understanding the relationship between parent and child, haha. With a 7 year old child of my own, I would expect to understand that by now!
So maybe I will give it another shot. It’s unfortunate that this topic is regarding imported images as backgrounds, rather than noob problems. But I’ll be sure to update here with the new issues I run into;)

Robert_Tompkins

Alright, made some quick changes to setup what was mentioned.
I attempted this on the Laser objects..
I will have to dive in a little more to determine where this is happening, but here is what I noticed.

Here is some misc. output I use for debugging that was produced when I noticed this occurring..

Generating Lasers with 992 left
timeToGen 2000 Lasers: 0.12253904342651367
generateMeteorLists: 0.006246089935302734
stage: 2
Generating Lasers with 2993 left
timeToGen 1000 Lasers: 0.07495284080505371
generateMeteorLists: 3.814697265625e-06
stage: 3
Generating Lasers with 3995 left
timeToGen 1000 Lasers: 0.08381533622741699
generateMeteorLists: 3.0994415283203125e-06
stage: 4
maxExecutionTimeUpdate: 0.1294388771057129
Max Star execution time: 0.0003571510314941406
Max Item Collision execution time: 0.0007109642028808594
Max Laser Collision execution time: 0.0007081031799316406
Max Spawn Item execution time: 0.00047898292541503906
Max Should Fire execution time: 0.002073049545288086
Active weapon: laser
Weapon Upgrades:
self.laserFireRateLevel: 50
self.laserNumberOfLasersLevel: 1
self.laserPowerLevel: 1
Dictionary of self.children and count of each:
: 230
: 5
: 1000
: 4987
: 170
: 51
: 3
len(listOfChildren): 6446
self.stageNumber: 4

I noticed that when firing lasers, I should have been firing 10 projectiles each time, but some of the projectiles were not visible. So I set a breakpoint and found that some of the lasers in my list of projectiles (contains all projectiles that have been fired and should be visible) have no parent.

Unfortunately, the functions that may be involved in this are messy and I will need to dig around and clean up before I can efficiently get to root cause.

nol_03

[@Robert_Tompkins said:

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

  • list item]([link url](```

  • link url
    ```
    ))