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