如何使用Python中的turtle让物品同时绘制?

4

我有一个作业任务,需要让四个不同的海龟像行星围绕太阳一样移动。我已经把代码都写好了,只需要让这些海龟同时开始画图。我想知道是否有相对简单的方法让它们在合理的时间内开始运动?以下是代码:

def planets():
    """simulates motion of Mercury, Venus, Earth, and Mars"""
    import turtle

    mercury = turtle.Turtle()
    venus = turtle.Turtle()
    earth = turtle.Turtle()
    mars = turtle.Turtle()
    mercury.shape('circle')
    venus.shape('circle')
    earth.shape('circle')
    mars.shape('circle')
    mercury.pu()
    venus.pu()
    earth.pu()
    mars.pu()
    mercury.sety(-58)
    venus.sety(-108)
    earth.sety(-150)
    mars.sety(-228)
    mercury.pd()
    venus.pd()
    earth.pd()
    mars.pd()
    mars.speed(7.5)
    venus.speed(3)
    earth.speed(2)
    mars.speed(1)
    mercury.circle(58)
    venus.circle(108)
    earth.circle(150)
    mars.circle(228)

感谢您的提前支持!
5个回答

4

一般而言,如果您想同时做多件事情,有两种选择:

  • 抢占式多线程,在这种情况下,您只需为每个任务创建一个线程,它们都会尽力全速工作,计算机会安排它们之间的交替工作。
  • 合作式调度:您为一件事情完成一小部分工作,然后为下一个完成一小部分,以此类推,然后回到第一件事情。

在这种情况下,您需要的是第二种方式。(好吧,你可能想要第一种方式,但你无法实现;tkinter和因此turtle只能在主线程上运行。)例如,先绘制每个圆的前1°,然后是每个圆的下一个1°,以此类推。

那么,如何实现呢?circle方法有一个可选的extent参数,它表示要绘制的角度(以度为单位)。因此,您可以这样做:

for i in range(360):
    mercury.circle(58, 1)
    venus.circle(108, 1)
    earth.circle(150, 1)
    mars.circle(228, 1)

当然,您使extent数值越小,乌龟执行的“步骤”就会越多,所以它们绕行的速度就会更慢。
此外,我不确定您是否真的想使用speed。这会导致每次移动的动画变得更慢。这不影响它们绕太阳的速度,只影响每个步骤绘制所需的时间。因此,我认为您真正想做的是将所有速度设为0(没有动画延迟),但是使较快的行星每步移动更大的距离:
mercury.speed(0)
venus.speed(0)
earth.speed(0)
mars.speed(0)
for i in range(360):
    mercury.circle(58, 7.5)
    venus.circle(108, 3)
    earth.circle(150, 2)
    mars.circle(228, 1)

当然,这意味着水星将绕太阳转7.5次,而火星只会绕行一次...但这正是您所希望的,对吧?

谢谢!我没有想到那个。 - Tom Plutz
我认为你在说只有两种解决方案时,可能有点过于复杂化这个特定的问题了。例如,如果乌龟在刷新显示之前绘制了四个项目,它们实际上会同时被绘制。因此,这就成为了第三种方法。对于初学者来说,谈论线程和调度程序会让他们感到很困惑。(诚然,谈论延迟屏幕重绘也会让他们离开浅水区...) - Bryan Oakley
@BryanOakley:我非常确定他实际上想要看到轨道的动画效果,因此关闭更新并绘制它们是没有帮助的。但我可能错了。让我们看看他是否会进一步评论,如果适当的话,我将添加该选项。 - abarnert

1
在我的另一个答案中,我说过你必须进行某种协作调度,因为tkinter不是线程安全的。但这并不完全正确。tkinter是线程安全的,只是它没有任何一种分派机制来从后台线程发布事件到主循环,所以你必须添加队列或其他方法来实现它。
我绝对不建议在这里使用线程,但看看它如何工作还是值得的。
Allen B. Taylor聪明的mtTkinter库为您包装了所有的魔法。它不适用于Python 3,但我已经移植了它,你可以在GitHub上获得mttkinter。该模块没有安装程序;你需要将它复制到与planets.py相同的目录中。但是然后你就可以这样做:
import threading
import turtle
import mttkinter

def planets():
    """simulates motion of Mercury, Venus, Earth, and Mars"""
    # Use your existing code, up to...
    mars.speed(1)

    # Now create a thread for each planet and start them
    mercury_thread = threading.Thread(target=lambda: mercury.circle(58))
    venus_thread = threading.Thread(target=lambda: venus.circle(108))
    earth_thread = threading.Thread(target=lambda: earth.circle(150))
    mars_thread = threading.Thread(target=lambda: mars.circle(228))
    mercury_thread.start()
    venus_thread.start()
    earth_thread.start()
    mars_thread.start()

    # Unfortunately, if we just exit the function here, the main thread
    # will try to exit, which means it'll wait on all the background threads.
    # But since they're all posting events and waiting on the main thread to
    # reply, they'll deadlock. So, we need to do something tkinter-related
    # here, like:
    turtle.Screen().exitonclick()

planets()

1

由于之前的回答都没有提到turtle自己的ontimer()用于运行模拟,我决定实现一个使用它并使整个问题更加数据导向的方法:

from turtle import Turtle, Screen

""" Simulate motion of Mercury, Venus, Earth, and Mars """

planets = {
    'mercury': {'diameter': 0.383, 'orbit': 58, 'speed': 7.5, 'color': 'gray'},
    'venus': {'diameter': 0.949, 'orbit': 108, 'speed': 3, 'color': 'yellow'},
    'earth': {'diameter': 1.0, 'orbit': 150, 'speed': 2, 'color': 'blue'},
    'mars': {'diameter': 0.532, 'orbit': 228, 'speed': 1, 'color': 'red'},
}

def setup_planets(planets):
    for planet in planets:
        dictionary = planets[planet]
        turtle = Turtle(shape='circle')

        turtle.speed("fastest")  # speed controlled elsewhere, disable here
        turtle.shapesize(dictionary['diameter'])
        turtle.color(dictionary['color'])
        turtle.pu()
        turtle.sety(-dictionary['orbit'])
        turtle.pd()

        dictionary['turtle'] = turtle

    screen.ontimer(revolve, 50)

def revolve():
    for planet in planets:
        dictionary = planets[planet]
        dictionary['turtle'].circle(dictionary['orbit'], dictionary['speed'])

    screen.ontimer(revolve, 50)

screen = Screen()

setup_planets(planets)

screen.exitonclick()

输出时间快照

enter image description here


0
turtle.py以两个测试演示结束。第二个演示以一个海龟'tri'追逐另一个'海龟'而结束。它执行了第一篇帖子中abarnet的建议 - 在循环内进行增量操作。
while tri.distance(turtle) > 4:
    turtle.fd(3.5)
    turtle.lt(0.6)
    tri.setheading(tri.towards(turtle))
    tri.fd(4)

turtledemo包有多个示例可供学习。使用python -m turtledemo是启动查看器的简单方法。未来版本中已修复了一些错误。


0
其他用户发布的方法效果很好。然而,我用面向对象的设计做了一个类似的太阳系模型,我创建了一个名为System的类,在其中使用所需的高度和宽度来创建系统,并创建了一个名为stepAll的函数,该函数具有代理列表并将该列表中所有代理推进一步。
class System:
"""A two-dimensional world class."""

    def __init__(self, width, height):
        """Construct a new flat world with the given dimensions."""

        self._width = width
        self._height = height
        self._agents = { }
        self.message = None

    def getWidth(self):
        """Return the width of self."""

        return self._width

    def getHeight(self):
        """Return the height of self."""

        return self._height

    def stepAll(self):
        """All agents advance one step in the simulation."""

        agents = list(self._agents.values())
        for agent in agents:
            agent.step()

然后,我创建了一个行星类,并将行星设为代理,定义了它们在Step函数中的步骤。
class Planet:
"""A planet object"""

    def __init__(self, mySystem, distance, size, color, velocity, real_size, real_mass, name):
        self = self
        self._system = mySystem
        mySystem._agents[self] = self #make planet an agent
        self._velocity = velocity
        self._color = color
        self._distance = distance
        self._size = size
        self._position = [distance, distance - distance]
        self._angle = 90
        self._real_size = real_size
        self._real_mass = real_mass
        self._name = name

        #MAKE PLANET
        self._turtle = turtle.Turtle(shape = 'circle')
        self._turtle.hideturtle()

        #INITIALIZE PLANET
        self._turtle.speed('fastest')
        self._turtle.fillcolor(color)
        self._turtle.penup()
        self._turtle.goto(self._position)
        self._turtle.turtlesize(size,size,size)
        self._turtle.showturtle()


    def getmySystem(self):
        """Returns the system the planet is in"""
        return self._mySystem

    def getdistance(self):
        """Returns the distance the planet is from the sun"""
        return self._distance

    def getposition(self):
        """Returns the position of the planet"""
        return self._position

    def getvelocity(self):
        """Returns the velocity of the planet"""
        return self._velocity

    def step(self):
        """Moves the planet one step on its orbit according to its velocity and previous position"""
        xvar = self._position[0]
        yvar = self._position[1]

        newx = int(self._distance*math.cos(math.radians(90-self._angle)))
        newy = int(self._distance*math.sin(math.radians(90-self._angle)))

        self._turtle.goto(newx, newy)

        self._angle = self._angle - self._velocity

然后在我的main()函数中,我使用它们各自的值初始化了所有行星,并说:

while True:
    space.stepAll()

这样做可以实现你的目标,而且通过调用行星类来添加另一个具有特定参数的行星会更加容易,而不是绘制全新的行星并尝试单独移动它以及其他行星。

希望这能帮助到某些人!


你的最后两个语句update()exitonclick()永远不会被执行,因为前面的语句是一个无限循环:while True:。如果你点击窗口,它不会按照指定的方式退出。一个正确的turtle程序不应该包含无限循环,因为它会阻塞其他事件的触发。例如,如果你在行星/海龟上设置了onclick()事件,它们将不会响应。此外,你上面的代码缩进不正确。 - cdlane
我会修复代码缩进。感谢您指出“update()”和“exitonclick()”不会有任何作用的事实。但是,我有星球点击事件来显示信息,它们工作得很好。 - Isaiah Banta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接