1

I am working on a little project to have autonomous cells move around and eventually be a little game of life simulation. Currently I'm having an issue with randomizing the cells movement. I have the cell as a class and set the starting angle in the init then in a move function the angle is updated. For some reason the updated angle is reset the next time the move function is called. To handle the simulation window and physics I'm using Python Arcade with pymunk physics.

Cell Class

import arcade
import random
import math
from dice import Dice

cell_types = ["Plant", "Animal"]  # add fungus and virus later
d20 = Dice(20, 1)
count = 0


class Cell(arcade.SpriteCircle):
    """ Cell Sprite """

    def __init__(self, radius, color, soft, mass, x, y):
        """ Init """
        # initialize SpriteCircle parent class
        super().__init__(radius, color, soft)

        # body
        self.mass = radius * mass
        self.speed = radius
        self.center_x = x
        self.center_y = y
        self.angle = random.randint(0, 360)
        self.hit_box_algorithm = "Simple"

        # characteristics
        self.type = random.choice(cell_types)

    def move(self):
        # roll a d20
        roll = d20.roll()
        print(f"roll: {roll}")

        # if d20 is 15 or more turn right
        # if d20 is 5 or less turn left
        print(f"old angle: {self.angle}")
        if roll >= 15:
            self.angle -= 90
        elif roll <= 5:
            self.angle += 90
        print(f"new angle: {self.angle}")

        # convert angle to radians
        angle_rad = math.radians(self.angle)

        # find next coordinates and save as a tuple
        print(f"old x pos: {self.center_x}")
        print(f"old y pos: {self.center_y}")
        self.center_x += self.speed * math.cos(angle_rad)
        self.center_y += self.speed * math.sin(angle_rad)
        print(f"new x pos: {self.center_x}")
        print(f"new y pos: {self.center_y}")
        
        # return the tuple for apply force function
        movement_vector = (self.center_x, self.center_y)
        return movement_vector


Dice class for reference, it's just a way to have a randrange as an object instead of typing out the function each time and does function as expected otherwise the results later in the post would not have any variance between the old and new angles.

import random


class Dice:
    """ Create a die specifying sides and how many dice"""
    def __init__(self, sides, count=1):
        self.sides = sides
        self.count = count

    def roll(self):
        """ Roll the set of dice"""
        total = 0
        for i in range(self.count):
            total += random.randrange(1, self.sides)
        return total

The class is initialized as a "new_cell" and added into a spritelist and when the on_update function runs, the move function is called.

Relevant code for main.py using arcades boilerplate window template, segments of boilerplate not in use have been cut. https://api.arcade.academy/en/stable/examples/starting_template.html#starting-template

import arcade
import random
from typing import Optional

from cell import Cell
from dice import Dice

SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Autonomous Cells"

STARTING_CELL_COUNT = 1

SPRITE_SIZE = 32
SPRITE_SCALING = .15
CELL_SIZE = SPRITE_SIZE * SPRITE_SCALING
CELL_SIZE_MIN_MULTIPLIER = 1
CELL_SIZE_MAX_MULTIPLIER = 5

DEFAULT_DAMPING = .5
CELL_DAMPING = 0.4
CELL_FRICTION = 0.5
DEFAULT_CELL_MASS = 1.0
CELL_MAX_SPEED = 50


class MyGame(arcade.Window):
    """
    Main application class.

    NOTE: Go ahead and delete the methods you don't need.
    If you do need a method, delete the 'pass' and replace it
    with your own code. Don't leave 'pass' in this program.
    """

    def __init__(self, width, height, title):
        super().__init__(width, height, title)

        arcade.set_background_color(arcade.color.DARK_BLUE_GRAY)

        # If you have sprite lists, you should create them here,
        # and set them to None
        self.cell_sprite_list = None

        # physics engine
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

    def setup(self):
        """ Set up the game variables. Call to re-start the game. """
        # Create your sprites and sprite lists here
        self.cell_sprite_list = arcade.SpriteList()

        for i in range(STARTING_CELL_COUNT):
            new_color = (random.randrange(256),
                         random.randrange(256),
                         random.randrange(256)
                         )
            new_cell = Cell(radius=(random.randint(CELL_SIZE_MIN_MULTIPLIER,
                                                   CELL_SIZE_MAX_MULTIPLIER
                                                   ) * int(CELL_SIZE)),
                            color=new_color,
                            soft=False,
                            mass=DEFAULT_CELL_MASS,
                            x=SCREEN_WIDTH / 2 + random.randint(3, 10),
                            y=SCREEN_HEIGHT / 2 + random.randint(3, 10)
                            )
            self.cell_sprite_list.append(new_cell)

        # physics engine setup
        damping = DEFAULT_DAMPING
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=(0, 0))
        # add cell sprites to physics engine
        for cell in self.cell_sprite_list:
            self.physics_engine.add_sprite(cell,
                                           friction=CELL_FRICTION,
                                           collision_type="Cell",
                                           damping=CELL_DAMPING,
                                           max_velocity=CELL_MAX_SPEED)


    def on_draw(self):
        """
        Render the screen.
        """

        # This command should happen before we start drawing. It will clear
        # the screen to the background color, and erase what we drew last frame.
        self.clear()
        self.cell_sprite_list.draw()

        # Call draw() on all your sprite lists below

    def on_update(self, delta_time):
        """
        All the logic to move, and the game logic goes here.
        Normally, you'll call update() on the sprite lists that
        need it.
        """
        for cell in self.cell_sprite_list:
            self.physics_engine.apply_force(cell, cell.move())
        self.physics_engine.step()

def main():
    """ Main function """
    game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    game.setup()
    arcade.run()


if __name__ == "__main__":
    main()

move function called for each cell in the sprite list after this loop finishes the cells initial angle resets to the value at instantiation rather than retaining the newly applied value from the move function.

The function seems to work but after the for loop exits each cells angle variable is reset to its original state. Console log output shows that while in the for loop the angle is updated but after exiting the loop and running the next time the angle has been reset to the original value.

Angle before move called: 303.0
roll: 17
old angle: 303.0
new angle: 213.0
old x pos: 450.6516108053191
old y pos: 288.2189227316175
new x pos: 437.2328817181923
new y pos: 279.5046981713771
angle after move called: 213.0
Angle before move called: 303.0
roll: 3
old angle: 303.0
new angle: 393.0
old x pos: 451.40957273618153
old y pos: 287.87260184601035
new x pos: 464.8283018233083
new y pos: 296.5868264062508
angle after move called: 393.0

I have tried reworking the movement function and calling the movement function multiple times per update outside of the for loop. when called consecutively the angle does carry over to the next call but is reset to the original value by the time the next on_update function runs.

I was expecting the self.angle of the instanced cell in the cell_sprite_list to update to the new angle generated by the move function.

  • I was able to run your code. I see the cirlce is moving from the center of the screen toward random direction. What behaviour do you want to have instead? – Alderven Feb 02 '23 at 18:43
  • The angle is the direction that the circle is moving, the move function updates the angle to move in a different direction but instead is being reset to the original angle that was set when the sphere was instantiated. What I want to see is the angle being updated on the instantiated circle so it starts moving in that new direction. So that the sphere is moving in random directions as the program runs. – BackgorundLogic Feb 03 '23 at 19:08

1 Answers1

1

If you just need to move circle randomly, you can directly update its coordinates:

import arcade
import random


class Game(arcade.Window):

    def __init__(self):
        super().__init__()
        self.x = 400
        self.y = 300
        self.x_direction = random.choice([-1, 1])
        self.y_direction = random.choice([-1, 1])

    def on_draw(self):
        self.clear()
        arcade.draw_circle_filled(self.x, self.y, 30, arcade.color.RED)

    def on_update(self, delta_time):
        self.x += 3 * self.x_direction
        self.y += 3 * self.y_direction
        if random.random() < 0.1:
            self.x_direction = random.choice([-1, 1])
            self.y_direction = random.choice([-1, 1])


Game()
arcade.run()

Output:

enter image description here

Alderven
  • 7,569
  • 5
  • 26
  • 38