You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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会被重置为物理体的初始角度。

修复步骤

  1. 新增独立的direction_angle属性存储移动方向,不依赖Sprite自带的angle(避免被引擎覆盖)
  2. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 02:56:52