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.