4

I have a homework assignment, and I have to make four different turtles move like they are planets around the sun. I have it all written, its just a matter of making the turtles draw at the same time. I was wondering if there was a relatively easy way to make them start around the same time (within reason)? Anyway, here's the code:

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)

Thanks in advance!

cdlane
  • 40,441
  • 5
  • 32
  • 81
Tom Plutz
  • 77
  • 1
  • 6

5 Answers5

4

In general, if you want to do multiple things at the same time, there are two options:

  • Preemptive multithreading, where you just create a thread for each thing and they all try to work at full speed and the computer figures out how to interleave that work.
  • Cooperative scheduling: you do a small piece of work for one thing, then a small piece for the next, and so on, then come back to the first one.

In this case, it's the second one that you want. (Well, you might want the first, but you can't have it; tkinter, and therefore turtle, can only run on the main thread.) Draw, say, the first 1° of each circle, then the next 1° of each circle, and so on.

So, how do you do that? The circle method has an optional extent parameter, which is an angle (in degrees) to draw. So, you can do this:

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

Of course the smaller you make that extent value, the more "steps" each turtle is taking, so the slower they will take to orbit.

Also, I'm not sure you really want to use speed the way you're using it. That causes each move to animate more slowly. It doesn't affect how quickly they orbit around the sun, it just affects how long each step takes to draw. So I think what you really want to do here is leave all the speeds at 0 (no animation delay), but move the faster planets by a larger extent each step:

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)

Of course this means Mercury will end up orbiting the sun 7.5 times, while Mars will only orbit once… but that's exactly what you want, right?

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Thanks! I hadn't thought of that – Tom Plutz Sep 28 '14 at 08:55
  • I think you're overcomplicating this particular problem a bit when you say there are just two solutions. For example, if the turtle draws four items before refreshing the display they will for all intents and purposes be drawn at the same time. So, that becomes a third method. For a beginner, talking about threads and schedulers is pushing them into the deep end of the pool. (admittedly, talking about deferring screen redraws also takes them outside of the shallows...) – Bryan Oakley Oct 01 '14 at 21:31
  • @BryanOakley: I'm pretty sure he actually wants to see the orbits animating, in which case plotting them with updates off won't help. But I could be wrong. Let's see if be comments further, and if so I'll add that option if appropriate. – abarnert Oct 05 '14 at 18:02
1

In my other answer, I said that you have to do some kind of cooperative scheduling, because tkinter isn't thread-safe. But that isn't quite true. tkinter is thread-safe, it just doesn't have any kind of dispatching mechanism to post events on the main loop from a background thread, so you have to add a queue or some other way to do it.

I'd definitely not recommend using threads here, but it's worth seeing how it would work.

Allen B. Taylor's clever mtTkinter library wraps all of the magic up for you. It doesn't work with Python 3, but I've ported it, and you can get it as mttkinter on GitHub. The module have any installer; you'll have to copy it into the same directory as planets.py. But then you can do this:

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()
abarnert
  • 354,177
  • 51
  • 601
  • 671
1

Since none of the previous answers mention turtle's own ontimer() for running the simulation, I decided to do an implementation that uses it and makes the whole problem more data-oriented:

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()

OUTPUT SNAPSHOT IN TIME

enter image description here

cdlane
  • 40,441
  • 5
  • 32
  • 81
0

turtle.py ends with two test demos. The second one ends with one turtle 'tri' chasing another 'turtle'. It does what abarnet suggests in the first post -- increments within a loop.

while tri.distance(turtle) > 4:
    turtle.fd(3.5)
    turtle.lt(0.6)
    tri.setheading(tri.towards(turtle))
    tri.fd(4)

The turtledemo package has multiple examples to learn from. python -m turtledemo is the easy way to start the viewer. There are a couple of bugs that have been fixed for future releases.

Terry Jan Reedy
  • 18,414
  • 3
  • 40
  • 52
0

Methods posted by other users work well. However, I did a similar model of a solar system with object oriented design and what I did was create a class called System where I create a system with a desired height and width and created a stepAll function which has a list of agents and advances all agents in that list one 'step':

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()

Then, I created a Planet class, and made the planets agents, and defined what their step would be in a Step function.

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

Then in my main() function I initialized all the planets with their respective values and said:

while True:
    space.stepAll()

This accomplishes your goal and also makes it easier to add another planet later with the specific parameters you want it to contain by just calling the Planet Class, instead of drawing a completely new planet and trying to individually move it along with the others.

Hope this helps someone!

  • Your final two statements, `update()` and `exitonclick()` are never reached as the previous statement is an infinite loop: `while True:`. If you click on your window, it won't exit on click as specified. A proper turtle program shouldn't contain an infinite loop as it blocks other events from firing. For example, if you had `onclick()` events set on your planets/turtles, they won't respond. Also, your code indentation above isn't correct. – cdlane Dec 22 '16 at 02:57
  • I'll fix the code indentation. And thanks for pointing out the fact that 'update()' and 'exitonclick()' won't do anything. However, I have click events for planets to display information and they work just fine. – Isaiah Banta Dec 22 '16 at 08:50