Search code examples
pythonoptimizationrefreshpython-turtle

Problem understanding Python's turtle refreshing after using a tracer(0) and an update


Lately, I have been trying to create a fake 3D engine with Python. I say fake since it is not really creating a "Z" axis but instead refreshing and recreating what the user is supposed to see every frame. To do that, I am using a relatively basic Raycasting technique (I send multiple rays from my player and every time I send a new one, I slightly tilt their direction. Every time a ray touches a wall, the distance it traveled and its ray number (for example if the 45th ray collides with a wall, its number would be 45) are added to a list amongst other things. All of this is invisible to the user. But then, using this information, multiple vertical lines are drawn with different sizes depending on how far the wall is, or how far the ray has traveled, to reproduce perspective. The thing is, to do all of that, I am using a turtle.tracer(0) and a turtle.update() every time the user presses a key to move so that I can refresh the screen every time the character is supposed to move. The thing is, for some reason, every time the player moves, the screen not only takes a little bit of time to refresh but also turns black (the color of the background) before showing the new image, as if I was using a turtle.clear() outside a turtle.tracer(0), which I am not doing. Here's my code. I'm not really expecting anyone to read it because it's 250 lines of code but I did specify it.

import turtle
import random
import tkinter
xScreenSize = 1225
yScreenSize = 720
turtle.screensize(xScreenSize, yScreenSize)

def _3DTracer(height, RapportDistance, list,AngleTot2D, NbRay):
    """
    Entrees : height -> float , RapoortDistance -> float, list -> list, AngleTot2D -> int, NbRay -> int
    The list contains 4 elements : the current angle, the number of the ray, legth of the ray divided by the max one,
    the difference of brightness this ray has
    """
    #turtle.tracer(0)
    turtle.clear()
    turtle.pu()
    turtle.bgcolor(0,0,0)
    lenList = len(list)
    turtle.pensize(xScreenSize//AngleTot2D)
    WallYCenter = 0
    for f in range(lenList):
        colDif = 250 - list[f][3]
        if colDif > 255:
            colDif = 255
        print(colDif)
        turtle.color(0,0,colDif)
        currentX = list[f][1]/NbRay*xScreenSize - xScreenSize/2
        turtle.pu()
        turtle.goto(currentX, WallYCenter)
        turtle.left(-turtle.heading() - 90) # -> Sets dir to up
        WallHeight = yScreenSize - yScreenSize*list[f][2]
        turtle.forward(WallHeight/2)
        turtle.left(180)
        turtle.pd()
        turtle.forward(WallHeight)
    turtle.pensize(1)
    #turtle.update()
    return 'ok'

def Update3DScreen(NbAngle, NbTot):
    """
    Call: Update3DScreen(NbAngle,NbTot, OriginalAngle)
    Entrees : NbTot -> int
            OriginalAngle, NbAngle -> float
    Return: /
    Everytime this fonction is called, rays a created from the player's position and these rays keep moving strait
    until they collide a wall (using the CheckInsideColliders() fonction)or reach their max range. Then, the direction turns by
    NbAngle et it keeps going "NbTot" times. Everytime a wall is hit by a ray, it is added to the list
    """
    print('NbTot',NbTot)
    AngleEntreChaqueRayon = NbAngle/NbTot
    listOfCollisionsFoundInRay = []
    turtle.pd()
    turtle.tracer(0)
    turtle.goto(currentPos)
    turtle.left(-turtle.heading() + currentPlayerAngle)
    for i in range (NbTot):
        ColorDifference = 0
        hi = 'hi'
        shouldWrite = True
        FreeToMove = True
        count = 0
        tailleMaxLigne = 450
        while FreeToMove:
            turtle.forward(5)
            NotTouchedWall = CheckInsideColliders(FreeToMove, turtle.pos())
            if not NotTouchedWall:
                turtle.left(180)
                newCount =0
                while not NotTouchedWall:
                    turtle.forward(1)
                    NotTouchedWall = CheckInsideColliders(FreeToMove, turtle.pos())
                    newCount += 1
                turtle.left(180)
                listOfCollisionsFoundInRay.append([i * AngleEntreChaqueRayon, i,(count+5-newCount)/tailleMaxLigne, ColorDifference])
                FreeToMove = False
            else:
                newCount = 0
            if count >= tailleMaxLigne:
                FreeToMove = False
                shouldWrite = False
            count += 5 - newCount
            ColorDifference = count*255//tailleMaxLigne - 20

        turtle.goto(currentPos)
        turtle.left(AngleEntreChaqueRayon)
    if len(listOfCollisionsFoundInRay) != 0:
        _3DTracer(0.5, count/tailleMaxLigne, listOfCollisionsFoundInRay, NbAngle, NbTot)
    print(listOfCollisionsFoundInRay)
    turtle.update()
    turtle.left(-turtle.heading())

def CheckInsideColliders(Ok, Pos):
    """
    Call : CheckInsideColliders(bool, Tuple)
    Entrees : Ok -> bool
    Returns : Ok -> bool
    Verifies if the point Pos is situed in a collision zone, inside a wall. If it is, ok takes False,
    otherwise it takes True
    """
    LenColl = len(colliders)
    for hi in range(LenColl):
        if hi != 0 and hi%2 == 1:
            if PreviousPoints[0] > colliders[hi][0]: #le x du nv point est plus petit que celui d avant
                if PreviousPoints[1] > colliders[hi][1]:
                    if Pos[0] > PreviousPoints[0] or Pos[0] < colliders[hi][0] \
                            or Pos[1] > PreviousPoints[1] or Pos[1] < colliders[hi][1]:
                        Ok = True
                    else:
                        return False
                else:
                    # x current > previous but y current < previous
                    if Pos[0] > PreviousPoints[0] or Pos[0] < colliders[hi][0] or Pos[1] < \
                            PreviousPoints[1] or Pos[1] > colliders[hi][1]:
                        Ok = True
                    else:
                        return False
            else: # previous point x < current collider
                if PreviousPoints[1] > colliders[hi][1]:
                    if Pos[0] < PreviousPoints[0] or Pos[0] > colliders[hi][0] \
                        or Pos[1] > PreviousPoints[1] or Pos[1] < colliders[hi][1]:
                        Ok = True
                    else:
                        return False
                else : #previous point x < current collider and previous point y < current collider
                    if Pos[0] < PreviousPoints[0] or Pos[0] > colliders[hi][0] \
                        or Pos[1] < PreviousPoints[1] or Pos[1] > colliders[hi][1]:
                        Ok = True
                    else :
                        return False
        PreviousPoints = colliders[hi]
    return Ok
def up():
  """
  Is called every time the right arrow is pressed. It changes the player's position and then calls Update3DScreen
  to update the 3D perspective
  """
  turtle.tracer(0)
  turtle.goto(currentPos)
  turtle.left(-turtle.heading() + currentPlayerAngle)
  turtle.forward(7)
  globals()['currentPos'] = turtle.pos()
  turtle.clear()
  Update3DScreen(40,80)
  turtle.update()

def down():
  """
  Is called every time the right arrow is pressed. It changes the player's position and then calls Update3DScreen
  to update the 3D perspective
  """
  turtle.tracer(0)
  turtle.goto(currentPos)
  turtle.left(-turtle.heading() + currentPlayerAngle)
  turtle.forward(-7)
  globals()['currentPos'] = turtle.pos()
  turtle.clear()
  Update3DScreen(40,80)
  turtle.update()

def left():
  """
  Is called every time the right arrow is pressed. It changes the camera's direction and then calls Update3DScreen
  to update the 3D perspective
  """
  turtle.tracer(0)
  turtle.goto(currentPos)
  globals()['currentPlayerAngle'] -= 5
  turtle.clear()
  Update3DScreen(40,80)
  turtle.update()

def right():
    """
    Is called every time the right arrow is pressed. It changes the camera's direction and then calls Update3DScreen
    to update the 3D perspective
    """
    #globals()['currentPos'] = (currentPos[0] + 4, currentPos[1]) #globals()[variable] permet de changer une variable
    # globale dans une fonction
    turtle.tracer(0)
    turtle.goto(currentPos)
    globals()['currentPlayerAngle'] += 5
    turtle.clear()
    Update3DScreen(40, 80)
    #turtle.update()

def playerPos():
  """
  When the map is created, it creates a random position for the player to go to that is outside wall collisions. After that,
  it checks for key arrow presses and keeps track of the player's position
  """
  global currentPos
  basePosOk = False
  while not basePosOk:
      currentPos = [random.randint(- xScreenSize//2, xScreenSize//2),
                    random.randint(-yScreenSize//2, yScreenSize//2)]
      basePosOk = CheckInsideColliders(basePosOk, currentPos)
  print("currentPos : ", currentPos)
  turtle.pu()
  turtle.goto(currentPos)
  turtle.pd()
  global currentPlayerAngle
  currentPlayerAngle = 0
  turtle.dot(8)
  global currentPlayerOrientation
  currentPlayerOrientation = 0
  turtle.listen()
  turtle.pu()
  turtle.onkey(up, 'Up')
  turtle.onkey(down, 'Down')
  turtle.onkey(left, 'Left')
  turtle.onkey(right, 'Right')

def echap():
  """
  Fonctionnement : permet de continuer le programme donné dans cette fonction après avoir appuyé sur la touche "Echap"
  Allows the program to keep going when the used finished creating the 2D map by pressing the "escape" arrow. It then makes
  appear the 2D map that was just created and calls the playerPos() fonction.
  """
  print("\n")
  turtle.clear()
  lenColl = len(colliders)
  turtle.tracer(0)
  for i in range(lenColl):
    if i != 0 and i % 2 == 1:
      turtle.pu()
      turtle.goto(previousX, previousY)
      turtle.pd()
      turtle.goto(colliders[i][0], previousY)
      turtle.goto(colliders[i][0], colliders[i][1])
      turtle.goto(previousX, colliders[i][1])
      turtle.goto(previousX, previousY)
      turtle.pu()
    previousX = colliders[i][0]
    previousY = colliders[i][1]
    print(previousX, previousY)
  turtle.update()
  playerPos()

def get_mouse_click_coor(x, y):
  """
    :param x: float
    :param y: float
    Allows the user to create his own map by pressing anywhere on the screen. To keep track of where he pressed, turtle
    places dots on those locations
  """
  colliders.append([x, y])
  turtle.tracer(0)
  turtle.pu()
  turtle.goto(x,y)
  turtle.pd()
  turtle.dot(5)
  turtle.pu()
  turtle.update()
  print(x, y)

help(turtle.pos)
help(turtle.pensize)
turtle.colormode(255)
turtle.listen()  # fait en sorte que turtle vérifie tt le tps que certaines touches aient étées pressées
count = 0
colliders = []
turtle.onscreenclick(get_mouse_click_coor)
turtle.onkey(echap, "Escape")  # si l'utilisateur presse la touche "Echap", la fonction echap se lance
turtle.mainloop()
print(colliders)

To see where the problem was, I removed every tracer(0) and update() of the code but, somehow, turtle didn't animate anything. It was just acting as if the turtle.tracer(0) and turtle.update() fonctions were still there.


Solution

  • I've gone through your code and tried to sort out your tracer() issues among other tweaks. See if it now behaves closer to what you expect:

    from turtle import Screen, Turtle
    from random import randint
    
    xScreenSize = 1225
    yScreenSize = 720
    
    def _3DTracer(height, RapportDistance, ray_list, AngleTot2D, NbRay):
        """
        Entrees : height -> float, RapoortDistance -> float, ray_list -> list, AngleTot2D -> int, NbRay -> int
        The ray_list contains 4 elements : the current angle, the number of the ray, length of the ray divided by the max one,
        the difference of brightness this ray has
        """
    
        # screen.bgcolor('black')
    
        turtle.clear()
        turtle.penup()
    
        lenList = len(ray_list)
        turtle.pensize(xScreenSize // AngleTot2D)
        WallYCenter = 0
    
        for f in range(lenList):
            colDif = 250 - ray_list[f][3]
            if colDif > 255:
                colDif = 255
    
            turtle.color(0, 0, colDif)
            currentX = ray_list[f][1] / NbRay * xScreenSize - xScreenSize/2
            turtle.penup()
            turtle.goto(currentX, WallYCenter)
            turtle.left(-turtle.heading() - 90)  # -> Sets direction to up
            WallHeight = yScreenSize - yScreenSize * ray_list[f][2]
            turtle.forward(WallHeight/2)
            turtle.left(180)
            turtle.pendown()
            turtle.forward(WallHeight)
    
        turtle.pensize(1)
        screen.update()
    
    def Update3DScreen(NbAngle, NbTot):
        """
        Call: Update3DScreen(NbAngle, NbTot)
        Entrees:
            NbAngle -> float
            NbTot -> int
        Return: /
        Every time this function is called, rays are created from the player's position and these rays keep moving straight
        until they collide with a wall (using the CheckInsideColliders() function) or reach their maximum range. Then, the
        direction turns by NbAngle and it keeps going "NbTot" times. Every time a wall is hit by a ray, it is added to a list
        """
    
        AngleEntreChaqueRayon = NbAngle / NbTot
        listOfCollisionsFoundInRay = []
    
        turtle.pendown()
        turtle.goto(currentPos)
        turtle.left(currentPlayerAngle - turtle.heading())
    
        for i in range(NbTot):
            ColorDifference = 0
            # shouldWrite = True
            FreeToMove = True
            my_count = 0
            tailleMaxLigne = 450
    
            while FreeToMove:
                turtle.forward(5)
                NotTouchedWall = CheckInsideColliders(FreeToMove, turtle.position())
                if not NotTouchedWall:
                    turtle.left(180)
                    newCount = 0
    
                    while not NotTouchedWall:
                        turtle.forward(1)
                        NotTouchedWall = CheckInsideColliders(FreeToMove, turtle.position())
                        newCount += 1
    
                    turtle.left(180)
                    listOfCollisionsFoundInRay.append((i * AngleEntreChaqueRayon, i, (my_count+5 - newCount) / tailleMaxLigne, ColorDifference))
                    FreeToMove = False
                else:
                    newCount = 0
                if my_count >= tailleMaxLigne:
                    FreeToMove = False
                    # shouldWrite = False
                my_count += 5 - newCount
                ColorDifference = my_count*255 // tailleMaxLigne-20
    
            turtle.goto(currentPos)
            turtle.left(AngleEntreChaqueRayon)
    
        if len(listOfCollisionsFoundInRay) != 0:
            _3DTracer(0.5, count/tailleMaxLigne, listOfCollisionsFoundInRay, NbAngle, NbTot)
    
        screen.update()
        turtle.left(-turtle.heading())
    
    def CheckInsideColliders(Ok, Pos):
        """
        Call : CheckInsideColliders(bool, Tuple)
        Entrees : Ok -> bool
        Returns : Ok -> bool
        Verifies if the point Pos is situated in a collision zone, inside a wall. If it is, Ok goes False,
        otherwise it goes True
        """
    
        for i, collider in enumerate(colliders):
            if i != 0 and i%2 == 1:
                if PreviousPoints[0] > collider[0]:  # le x du nv point est plus petit que celui d avant
                    if PreviousPoints[1] > collider[1]:
                        if Pos[0] > PreviousPoints[0] or Pos[0] < collider[0] or Pos[1] > PreviousPoints[1] or Pos[1] < collider[1]:
                            Ok = True
                        else:
                            return False
                    else:
                        # x current > previous but y current < previous
                        if Pos[0] > PreviousPoints[0] or Pos[0] < collider[0] or Pos[1] < PreviousPoints[1] or Pos[1] > collider[1]:
                            Ok = True
                        else:
                            return False
                else:  # previous point x < current collider
                    if PreviousPoints[1] > collider[1]:
                        if Pos[0] < PreviousPoints[0] or Pos[0] > collider[0] or Pos[1] > PreviousPoints[1] or Pos[1] < collider[1]:
                            Ok = True
                        else:
                            return False
                    else:  # previous point x < current collider and previous point y < current collider
                        if Pos[0] < PreviousPoints[0] or Pos[0] > collider[0] or Pos[1] < PreviousPoints[1] or Pos[1] > collider[1]:
                            Ok = True
                        else:
                            return False
    
            PreviousPoints = collider
    
        return Ok
    
    def up():
        """
        Is called every time the right arrow is pressed. It changes the player's
        position and then calls Update3DScreen to update the 3D perspective
        """
    
        global currentPos
    
        turtle.goto(currentPos)
        turtle.left(currentPlayerAngle - turtle.heading())
        turtle.forward(7)
        currentPos = turtle.position()
        turtle.clear()
        Update3DScreen(40, 80)
        screen.update()
    
    def down():
        """
        Is called every time the right arrow is pressed. It changes the player's
        position and then calls Update3DScreen to update the 3D perspective
        """
        global currentPos
    
        turtle.goto(currentPos)
        turtle.left(currentPlayerAngle - turtle.heading())
        turtle.forward(-7)
        currentPos = turtle.position()
    
        turtle.clear()
        Update3DScreen(40, 80)
        screen.update()
    
    def left():
        """
        Is called every time the right arrow is pressed. It changes the camera's
        direction and then calls Update3DScreen to update the 3D perspective
        """
        global currentPlayerAngle
    
        turtle.goto(currentPos)
        currentPlayerAngle -= 5
        turtle.clear()
        Update3DScreen(40, 80)
        screen.update()
    
    def right():
        """
        Is called every time the right arrow is pressed. It changes the camera's
        direction and then calls Update3DScreen to update the 3D perspective
        """
    
        global currentPlayerAngle
    
        turtle.goto(currentPos)
        currentPlayerAngle += 5
        turtle.clear()
        Update3DScreen(40, 80)
        screen.update()
    
    def playerPos():
        """
        When the map is created, it creates a random position for the player to
        go to that is outside wall collisions. After that,
        """
    
        global currentPos, currentPlayerAngle, currentPlayerOrientation
    
        basePosOk = False
    
        while not basePosOk:
            currentPos = randint(-xScreenSize//2, xScreenSize//2), randint(-yScreenSize//2, yScreenSize//2)
            basePosOk = CheckInsideColliders(basePosOk, currentPos)
    
        turtle.penup()
        turtle.goto(currentPos)
        turtle.pendown()
    
        currentPlayerAngle = 0
        turtle.dot(8)
        currentPlayerOrientation = 0
        turtle.penup()
    
        screen.update()
    
    def echap():
        """
        Fonctionnement : permet de continuer le programme donné dans cette fonction après avoir appuyé sur la touche "Echap"
        Allows the program to keep going when the user finished creating the 2D map by pressing the "escape" arrow. It then
        makes the 2D map appear that was just created and calls the playerPos() function.
        """
        turtle.clear()
    
        for i, collider in enumerate(colliders):
            if i != 0 and i % 2 == 1:
                turtle.penup()
                turtle.goto(previousX, previousY)
                turtle.pendown()
    
                turtle.goto(collider[0], previousY)
                turtle.goto(collider)
                turtle.goto(previousX, collider[1])
                turtle.goto(previousX, previousY)
                turtle.penup()
    
            previousX, previousY = collider
    
        screen.update()
        playerPos()
    
    def get_mouse_click_coor(x, y):
        """
        :param x: float
        :param y: float
    
        Allows the user to create his own map by pressing anywhere on the screen. To keep
        track of where he pressed, turtle places dots on those locations
        """
    
        colliders.append((x, y))
    
        turtle.penup()
        turtle.goto(x, y)
        turtle.dot(5)
        screen.update()
    
    screen = Screen()
    screen.setup(xScreenSize, yScreenSize)
    screen.tracer(False)
    screen.colormode(255)
    
    turtle = Turtle()
    
    count = 0
    colliders = []
    
    currentPos = None
    currentPlayerAngle = None
    currentPlayerOrientation = None
    
    screen.onkey(up, 'Up')
    screen.onkey(down, 'Down')
    screen.onkey(left, 'Left')
    screen.onkey(right, 'Right')
    screen.onkey(echap, "Escape")  # si l'utilisateur presse la touche "Echap", la fonction echap se lance
    screen.listen()  # fait en sorte que turtle vérifie tt le tps que certaines touches aient étées pressées
    
    screen.onclick(get_mouse_click_coor)
    
    screen.mainloop()