Skip to content

Instantly share code, notes, and snippets.

@entwanne
Last active April 5, 2020 09:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save entwanne/234f464865da7fba6a3da1875db9d571 to your computer and use it in GitHub Desktop.
Save entwanne/234f464865da7fba6a3da1875db9d571 to your computer and use it in GitHub Desktop.
Zolfenstein
from zolf.game import Game
from zolf.game import Map
from zolf.game import Vector
from zolf.graphics import OptimizedApp as App
mapstring = '''
XXXXXXX
X XX
X X
X X
X X
XX X
XXX XXX
XXX XXX
XX XXX
XX XXXX
XXXXXXX
'''
def main():
game = Game((800, 600), Map.loads(mapstring), Vector(2.5, 2.1), 0.1)
App(game).run()
from zolf import main
if __name__ == '__main__':
main()
import math
from zolf.vector import Vector
from zolf.vector import segment_inters
class Map:
sides = {
('x', False): 'L',
('x', True): 'R',
('y', False): 'U',
('y', True): 'D'
}
def __init__(self, walls):
self.walls = walls
@classmethod
def loads(cls, mapstring, cwall='X'):
return cls({
(x, y): c == cwall
for y, line in enumerate(mapstring.strip('\n').splitlines())
for x, c in enumerate(line)
})
def map_inters(self, origin, vector):
vorient = Vector(vector.x < 0, vector.y < 0)
for k, position, side in segment_inters(origin, vector):
orient = getattr(vorient, side)
if orient: # If seen from the right/bottom, get the left/up block
setattr(position, side, getattr(position, side) - 1)
if side == 'x':
offset = position.y - math.floor(position.y)
else:
offset = position.x - math.floor(position.x)
case = (math.floor(position.x), math.floor(position.y))
if case not in self.walls:
break
yield (k, case, self.sides[side, orient], offset)
def wall_inter(self, origin, vector):
for k, case, side, offset in self.map_inters(origin, vector):
if self.walls[case]:
return (k, case, side, offset)
class Player:
def __init__(self, map, position, angle=0, speed=0.1):
self.map = map
self.position = position
self.dangle = angle
self.speed = speed
@property
def angle(self):
return math.radians(self.dangle)
@property
def sight(self):
return Vector(math.cos(self.angle), -math.sin(self.angle))
@property
def screen(self):
"direction vector of screen"
return Vector(-self.sight.y, self.sight.x)
def rotate(self, degrees):
self.dangle += degrees
def walk(self, direction=1):
self.position += self.sight * direction * self.speed
def wall_inter(self, dx): # -1. <= dx < 1.
"Get wall size for all columns of the screen"
vector = Vector(self.sight.x + dx * self.screen.x, self.sight.y + dx * self.screen.y)
inter = self.map.wall_inter(self.position, vector)
if inter is None:
return (0, None, None, 0)
k, case, side, offset = inter
h = 1 / k if k else 1.
return (h, case, side, offset)
class Game:
def __init__(self, screen_size, map, player_position, player_speed=0.1):
self.width, self.height = screen_size
self.map = map
self.player = Player(map, player_position, speed=player_speed)
def get_wall(self, x):
dx = 2 * x / self.width - 1
return self.player.wall_inter(dx)
def get_walls(self):
for x in range(self.width):
ret = self.get_wall(x)
yield (x,) + ret
def get_height(self, h):
return math.floor(min(h, 1.) * self.height)
import random
import pyglet
from zolf.optimize import WallsOptimizer
from zolf.optimize import best_indices
COLORS = {
'G': (128,128,128,255), # ground
'S': (100,100,255,255) # sky
}
TEXTURE_OFFSETS = {
'L': 0, # left
'R': 0.25, # right
'U': 0.5, # up
'D': 0.75, # down
}
def load_texture(filename):
texture = pyglet.image.load(filename).get_texture()
# Avoid aliasing
pyglet.gl.glTexParameteri(
pyglet.gl.GL_TEXTURE_2D,
pyglet.gl.GL_TEXTURE_MAG_FILTER,
pyglet.gl.GL_NEAREST,
)
return texture
class Canvas:
def __init__(self, game):
self.game = game
self.width, self.height = game.width, game.height
self.walls = []
self.batch = pyglet.graphics.Batch()
self._init_batch()
def _init_batch(self):
texture = load_texture('texture.png')
bg_group = pyglet.graphics.OrderedGroup(0)
fg_group = pyglet.graphics.TextureGroup(
texture,
pyglet.graphics.OrderedGroup(1),
)
for x in range(1, self.width+1):
# Ground batch
self.batch.add(2,
pyglet.gl.GL_LINES,
bg_group,
('v2i', (x, 0, x, self.height//2)),
('c4B', COLORS['G']*2),
)
# Sky batch
self.batch.add(2,
pyglet.gl.GL_LINES,
bg_group,
('v2i', (x, self.height//2, x, self.height)),
('c4B', COLORS['S']*2),
)
# Walls batches
self.walls.append(
self.batch.add(2,
pyglet.gl.GL_LINES,
fg_group,
('v2i', (x, 0, x, 0)),
('t2f', (0,0,0,1)),
)
)
def draw(self):
self.batch.draw()
def redraw_wall(self, x, height, side, offset):
wall = self.walls[x]
x += 1 # opengl starts X at 1
# Setup wall position (height)
line_height = self.game.get_height(height)
y = (self.height - line_height) // 2
wall.vertices = (x, y, x, y + line_height)
# Setup wall texture
x_offset = TEXTURE_OFFSETS[side] + offset / len(TEXTURE_OFFSETS)
y_offset = 0 if height <= 1 else (height-1) / (2*height)
wall.tex_coords = (x_offset, y_offset, x_offset, 1-y_offset)
class Window:
def __init__(self, canvas):
self.canvas = canvas
self.window = pyglet.window.Window(width=canvas.width, height=canvas.height)
self.window.set_exclusive_mouse(True)
self.keys = pyglet.window.key.KeyStateHandler()
self.window.push_handlers(self.keys)
self.window.event(self.on_draw)
self.window.event(self.on_expose)
def on_draw(self):
self.canvas.draw()
def on_expose(self):
pass
class App:
def __init__(self, game):
self.game = game
self.canvas = Canvas(game)
self.window = Window(self.canvas)
pyglet.clock.schedule_interval(self.update, 0.01)
def update(self, dt):
updated = False
if self.window.keys[pyglet.window.key.LEFT]:
self.game.player.rotate(3)
updated = True
elif self.window.keys[pyglet.window.key.RIGHT]:
self.game.player.rotate(-3)
updated = True
if self.window.keys[pyglet.window.key.UP]:
self.game.player.walk()
updated = True
elif self.window.keys[pyglet.window.key.DOWN]:
self.game.player.walk(-1)
updated = True
if updated:
self.redraw_walls()
def redraw_walls(self):
for x, height, _, side, offset in self.game.get_walls():
self.canvas.redraw_wall(x, height, side, offset)
def run(self):
self.redraw_walls()
pyglet.app.run()
class OptimizedApp(App):
def __init__(self, game):
super().__init__(game)
self.indices = best_indices(game.width)
def redraw_walls(self):
walls = WallsOptimizer(self.indices)
for x in walls.indices:
height, case, side, offset = self.game.get_wall(x)
if case is None:
continue
walls.add_wall((case, side), x, height, offset)
for (_, side), x, height, offset in walls.walls:
self.canvas.redraw_wall(x, height, side, offset)
from collections import namedtuple
_Wall = namedtuple('_Wall', ('x', 'height', 'offset'))
class WallsOptimizer:
# Optimize to avoid calculating multiple times for a same wall block
def __init__(self, indices):
self._indices = list(indices)
self.cache = {}
@property
def indices(self):
while self._indices:
yield self._indices.pop()
def add_wall(self, key, x, height, offset):
wall = _Wall(x, height, offset)
if key in self.cache:
left, right = self.cache[key]
if wall.x < left.x:
self.cache[key] = (wall, right)
for i in range(wall.x + 1, left.x):
self._indices.remove(i)
else: # x > right.x
self.cache[key] = (left, wall)
for i in range(right.x + 1, wall.x):
self._indices.remove(i)
else:
self.cache[key] = (wall, wall)
@property
def walls(self):
for key, (left, right) in self.cache.items():
height_slope = offset_slope = 0
amplitude = right.x - left.x
if amplitude:
height_slope = (right.height - left.height) / amplitude
offset_slope = (right.offset - left.offset) / amplitude
for x in range(left.x, right.x + 1):
height = left.height + (x - left.x) * height_slope
offset = left.offset + (x - left.x) * offset_slope
yield key, x, height, offset
def best_indices(width):
def _get_indices(indices):
if len(indices) > 2:
pivot = len(indices) // 2
yield indices[pivot]
yield from _get_indices(indices[:pivot])
yield from _get_indices(indices[pivot+1:])
else:
yield from indices
indices = range(width)
return [indices[0], *_get_indices(indices[1:-1]), indices[-1]]
import math
import typing
from dataclasses import dataclass
@dataclass
class Vector:
x: float
y: float
def __mul__(self, k: float) -> 'Vector':
return type(self)(self.x * k, self.y * k)
def __rmul__(self, k: float) -> 'Vector':
return self * k
def __add__(self, v: 'Vector') -> 'Vector':
return type(self)(self.x + v.x, self.y + v.y)
def segment_coord_inters(origin: float, dist: float): # may be infinite
"""
Intersections between a 1D segment and integers
yields (k, pos) couples such that origin + k * dist == pos, where pos is an integer
ordered by k
"""
if dist:
step = abs(1 / dist)
if dist > 0:
direction = 1
position = math.ceil(origin)
else:
direction = -1
position = math.floor(origin)
k = (position - origin) / dist
while True:
yield (k, position)
k += step
position += direction
def segment_inters(origin: Vector, vector: Vector): # infinite
"""
Intersections between a 2D segment and integer cases
yields (k, pos, dim) tuples such that origin + k * vector == pos, where the dim value of pos is an integer
ordered by k
"""
xgen = segment_coord_inters(origin.x, vector.x)
ygen = segment_coord_inters(origin.y, vector.y)
x = y = None
while True:
if x is None:
kx, x = next(xgen, (float('inf'), 0))
if y is None:
ky, y = next(ygen, (float('inf'), 0))
if kx < ky:
yield (kx, Vector(x, origin.y + kx * vector.y), 'x')
x = None
else:
yield (ky, Vector(origin.x + ky * vector.x, y), 'y')
y = None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment