Python Arcade中Cell类move函数执行后angle值重置问题
自主细胞移动模拟中Angle重置问题
问题描述
开发基于Python Arcade + Pymunk的生命游戏模拟项目,Cell类在__init__中初始化角度值,move方法更新角度,但每次on_update循环调用move时,角度都会被重置为实例化时的初始值。同一on_update内多次调用move时角度能保留,但跨循环就失效。
Cell类代码
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类代码
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
主程序代码
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函数执行时角度更新正常,但下一次on_update调用move时,angle会重置为实例化时的初始值。控制台日志示例:
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
解决方案
问题核心是Pymunk物理引擎会覆盖Sprite的center_x/center_y和angle属性。调用physics_engine.step()时,引擎会根据物理模拟结果更新Sprite的位置和角度,直接修改的self.angle会被重置为物理体的初始角度。
修复步骤
- 新增独立的
direction_angle属性存储移动方向,不依赖Sprite自带的angle(避免被引擎覆盖) - 修改
move方法逻辑:不再直接修改Sprite位置,而是计算力的方向向量,基于独立角度施加力
修改后的Cell类关键部分
class Cell(arcade.SpriteCircle): """ Cell Sprite """ def __init__(self, radius, color, soft, mass, x, y): """ Init """ super().__init__(radius, color, soft) self.mass = radius * mass self.speed = radius self.center_x = x self.center_y = y # 独立存储移动方向角度,不受物理引擎影响 self.direction_angle = random.randint(0, 360) self.hit_box_algorithm = "Simple" self.type = random.choice(cell_types) def move(self): roll = d20.roll() print(f"roll: {roll}") print(f"old direction angle: {self.direction_angle}") if roll >= 15: self.direction_angle -= 90 elif roll <= 5: self.direction_angle += 90 # 角度取模,避免数值过大 self.direction_angle %= 360 print(f"new direction angle: {self.direction_angle}") angle_rad = math.radians(self.direction_angle) # 计算力的方向向量,而非直接修改位置 force_x = self.speed * math.cos(angle_rad) force_y = self.speed * math.sin(angle_rad) print(f"current x pos: {self.center_x}") print(f"current y pos: {self.center_y}") return (force_x, force_y)
主程序on_update方法无需额外修改
move返回的力向量直接传给apply_force即可,物理引擎会负责更新Sprite位置:
def on_update(self, delta_time): for cell in self.cell_sprite_list: self.physics_engine.apply_force(cell, cell.move()) self.physics_engine.step()
原理说明
- Pymunk物理引擎管理Sprite对应的物理体,
step()方法会根据物理规则更新Sprite的位置和角度,直接修改Sprite属性会被引擎覆盖 - 使用独立的
direction_angle存储移动方向,不受物理引擎干扰,确保每次move调用都能基于上一次的方向更新
内容的提问来源于stack exchange,提问作者BackgorundLogic
相关产品推荐
相关产品推荐

