'How to control Atlas images in memory with CoreImage, Python and Kivy

So i'm making this arcade game and i have a state machine that takes an object(the enemy class) as a constructor parameter, and i have 1 folder for the enemy sprite.

So when i instantiate 1 state machine the game works fine, but when i instantiate 2 state machines(2 enemies) things get weird, when 1 enemy gets to the end of the screen and turn around with kivy's "flip_horizontal" the other enemy turns around too, and its because both state machines are controlling the same enemy folder(same images).

Now if i create an enemy folder for each state machine then it works fine but this is not good. so i thought about manipulating the images in memory, this way the effects of a state machine on the images wont be shared to other state machines. I tried to do this using CoreImage but the furthest i got was to get a static on the screen and move the "x position", no animation, nothing. So would anyone help me with this? or if you have a better solution i will be grateful.

heres a sample code: statemachine.py:

from kivy.app import App
from kivy.properties import Clock, StringProperty, NumericProperty
from kivy.atlas import Atlas
from kivy.uix.image import Image
from itertools import cycle
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.widget import Widget
from kivy.core.window import Window


class EnemyFSM(FloatLayout):

    def __init__(self, enemy_object, **kwargs):
        super().__init__(**kwargs)

        self.enemy = enemy_object

        self.states = {'ROAM': 'ROAM',
                'CHASE': 'CHASE',
                'ATTACK': 'ATTACK',
                'DEAD': 'DEAD',
                'IDLE': 'IDLE'}

        self.state = self.states['ROAM']
        self.init_state = True

    def update(self, dt):

        if self.state == 'ROAM':
            self.roam_state()

    def idle_state(self):
        self.enemy.methods_caller("walk")

    def roam_state(self):
        self.enemy.methods_caller("walk")

main.py:

from kivymd.app import MDApp
from kivymd.uix.screen import MDScreen
from kivymd.uix.floatlayout import MDFloatLayout
from kivy.properties import Clock
from kivy.uix.image import Image
from kivy.properties import NumericProperty
from random import uniform as rdm
from random import randint as rdi
from random import choice

enemy_list = []       

from baseenemy import Enemy
from statemachine import EnemyFSM


class LevelOne(MDScreen):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        Clock.schedule_once(self.setup, 1/60)


    def setup(self, dt):

        self.left_side_spawn = rdi(-500, -100)
        self.right_side_spawn = rdi(500, 600)
        self.spawn_side = [self.left_side_spawn, self.right_side_spawn]
        self.speed = rdi(1, 5)

        """here we instatiate the enemies, 1 works fine, 2 or more then hell breaks loose unless we give each state machine its own image folder"""
        for _ in range(1):
            self.base_enemy = Enemy(choice(self.spawn_side), rdm(1, 5)) 
            self.add_widget(self.base_enemy)

            self.enemy_ai = EnemyFSM(self.base_enemy)
            self.add_widget(self.enemy_ai)
            enemy_list.append(self.enemy_ai)

        Clock.schedule_interval(self.init_game, 1/60)


    def init_game(self, dt):

        for i in range(len(enemy_list)):
            enemy_list[i].update(dt)


class MainApp(MDApp):
    def build(self):
        self.theme_cls.theme_style = "Dark"
        self.theme_cls.primary_palette = "Red"

        return None

    def on_start(self):
        self.fps_monitor_start()

main.kv:

LevelOne:


<EnemyFSM>:

<Enemy>:
    source: root.frame_path #the frame_path variable in the enenmy class 
    size_hint: None, None
    size: "150dp", "150dp"
    x: root.xpos
    y: root.ypos

baseenemy.py:

from kivy.app import App
from kivy.atlas import Atlas
from kivy.properties import Clock, StringProperty, NumericProperty
from kivy.uix.image import Image
from kivy.core.image import Image as CoreImage
import itertools

"""Method to set up the frame names"""
def frame_setup(src):
    return [f"1_enemies_1_{src}_{i}" for i in range(20)]

"""Path to the image folder with the atlas"""
def create_frame_path(frame_name):
    return {'my_source': 
            f"atlas://{frame_name}/{frame_name}_atlas/1_enemies_1_{frame_name}_0",
            'my_atlas': f"atlas://{frame_name}/{frame_name}_atlas/"}

class Enemy(Image):

    xpos = NumericProperty(10)
    ypos = NumericProperty(50)

    init_frame = create_frame_path('walk')#default frame state 'walk'

    frame_path = StringProperty(init_frame['my_source'])#path to the image, this var is also the images source in the .kv file

    frame_atlas_path = None
    updated_frames = []
    previous_frame = None
    frame_cycle_called = False
    updated_frames_cycle = None
    fliped = 0

    def __init__(self, spawn_pos, speed, *args, **kwargs):
        super().__init__(**kwargs)

        self.goal = 0
        self.methods_dict = {'walk': self.walk}

        self.methods_call = None
        self.step = 3
        self.xpos = spawn_pos

    """this method, takes a param, and sets up everything
        the new frame if the frame is diferent than the current frames'walk'
        creates the frame list..."""
    def methods_caller(self, new_src):

        #The src param is the state of the enemy
        #which will tell which frames to set up
        #now its just "walk"
        self.src = new_src
        self.updated_frames = frame_setup(self.src)

        #if a state change has occured these lines will run and set up
        # a new set of frames according to the new state but for now its just the 'walk' state
        if self.previous_frame is not new_src:
            self.previous_frame = new_src

            self.methods_call = self.methods_dict[new_src]#this get the dict element which is a function and assign to the var
            self.frame_path = str(create_frame_path(new_src))
            self.frame_atlas_path = create_frame_path(new_src)
            self.frame_cycle_called = False

        if not self.frame_cycle_called:
            self.updated_frames_cycle = itertools.cycle(self.updated_frames)
            self.frame_cycle_called = True

        self.methods_call()#calling the method we got from the dict element which call the walk method

    def walk(self):
        #this is where the walking animation happen
        if self.xpos >= 700:
            self.goal = 1
        if self.xpos <= -300:
            self.goal = 0

        self.frame_path = 
self.frame_atlas_path['my_atlas']+next(self.updated_frames_cycle)

    
        image_side = Image(source=self.frame_path).texture

        if self.goal == 0 and int(round(image_side.tex_coords[0])) != 0:
            image_side.flip_horizontal()
            self.fliped = 0
    
        if self.goal == 1 and int(round(image_side.tex_coords[0])) != 1:
            image_side.flip_horizontal()
            self.fliped = 1
    
        if self.fliped == 0:
            self.xpos += self.step

        if self.fliped == 1:
            self.xpos -= self.step

And finally heres the link to a zip file with the image folder: https://drive.google.com/file/d/11eGTS_pI690eI5EhxAAoRmT05uNW3VOM/view?usp=sharing

and sorry for the long text.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source