Dies ist eine alte Version des Dokuments!
Download the release version here:
neg-world-engine.zip
Or download the newest version from the Git repository:
https://gitlab.tubit.tu-berlin.de/srather/NEG-World-Engine.git
import colors from vector import * class drawable(list): """ Abstract superclass for all objects drawable on canvas. A drawable is a list of points ('Vector') with a 'color' attribute. """ def __init__(self, *points, color=colors.BLACK): """ Overrides '__init__' method of 'list' class. Will only accept point-like objects and adds 'color' attribute. """ list.__init__(self) for point in points: drawable.append(self, point) self.color = color def append(self, item): """ Overrides 'append' method of 'list' class. Will only accept point-like objects. """ try: list.append(self, Vector(*item)) except: raise TypeError(f"Expected vector/point, but got {item.__class__.__name__}") def __str__(self): """ Override '__str__' method of 'list' class. Return string with classname, points in 'self' and additional attributes. """ return f"{self.__class__.__name__}({list.__repr__(self)[1:-1]}) with {self.__dict__.__repr__()[1:-1].replace(':', ' =').replace(' ', '')}" def __repr__(self): """ Override '__repr__' method of 'list' class. Return string with classname, points in 'self' and additional attributes. """ return f"{self.__class__.__name__}({list.__repr__(self)[1:-1]}, {self.__dict__.__repr__()[1:-1].replace(':', ' =').replace(' ', '')})" def draw(self, screen, offset): """Abstract method to draw 'self' on 'screen'.""" raise NotImplementedError def draw_shadow(self, screen, offset, source, color=(31,31,31)): """Abstract method to draw a shadow of 'self' on 'screen'.""" raise NotImplementedError def dist_to(self, source): """Abstract method to calculate the distance to the player ('source').""" raise NotImplementedError def cut(self, source, portal): """Abstract method to cut 'self' to fit in the portal view.""" raise NotImplementedError
import pygame.gfxdraw import colors from drawable import * from matrix import * class Ray(drawable): """ Ray class as drawable object for visual debug. A Ray has a starting point and a direction vector. """ def __init__(self, start, dir, color=colors.BRIGHT_RED): """Initilise 'self' with standard color 'BRIGHT_RED'.""" drawable.__init__(self, start, dir, color=color) def __getattr__(self, attr): """ Return 'self[0]' and 'self[1]' as attribute 'start' and 'dir'. """ if attr == 'start': return self[0] if attr == 'dir': return self[1] def point_at(self, value): """ Return the point 'self.start + value * self.dir'. """ if type(value) in (int, float): return self.start + value * self.dir raise TypeError(f"Expected int or float, but {value.__class__.__name__} found") def value_at(self, point): """ Return the value with 'point_at(value) == point'. """ if type(point) is Vector: if self.dir.multiple_of(point - self.start): value = (point - self.start) / self.dir if value is not None: return value # hotfix h = (point - self.start).dot(self.dir) / self.dir.dot(self.dir) print(f"Warning: returned nearest value for 'value_at' of point {((point - self.start) - h * self.dir).length()} off the ray)") return h raise TypeError(f"Expected Vector, but {point.__class__.__name__} found") def offscreen(self, screen, offset): """ Return a point of the ray that is not on the rect 'screen'. This assumes all rays go away from the 'screen' center. """ if not self.dir: # == 0 return self.start x, y = float('inf'), float('inf') if self.dir.x > 0: x = (offset.x + screen.get_rect()[2] / 2 - self.start.x) / self.dir.x elif self.dir.x < 0: x = (offset.x - screen.get_rect()[2] / 2 - self.start.x) / self.dir.x if self.dir.y > 0: y = (offset.y + screen.get_rect()[3] / 2 - self.start.y) / self.dir.y elif self.dir.y < 0: y = (offset.y - screen.get_rect()[3] / 2 - self.start.y) / self.dir.y return self.point_at(max(0, min(x, y))) def intersect(self, other): """ Return list of intersections sorted by distance to 'self.start'. """ if self.dir.length() == 0: return [] # point if isinstance(other, Vector): if self.dir.multiple_of(other - self.start): # point on self if self.value_at(other) is not None: return [other] return [] if isinstance(other, drawable): if type(other) is Ray: if self.dir.multiple_of(other.dir): # linear depentant return self.intersect(other.start) if self.dir.x == 0: # switch vectors for gauss algorithm return other.intersect(self) matrix = [ [self.dir.x, -other.dir.x, other.start.x - self.start.x], [self.dir.y, -other.dir.y, other.start.y - self.start.y], ] # gauss algorithm matrix[0][1] /= matrix[0][0] matrix[0][2] /= matrix[0][0] matrix[1][1] -= matrix[1][0] * matrix[0][1] matrix[1][2] -= matrix[1][0] * matrix[0][2] matrix[1][2] /= matrix[1][1] matrix[0][2] -= matrix[0][1] * matrix[1][2] if matrix[0][2] >= 0 and matrix[1][2] >= 0: return [self.point_at(matrix[0][2])] return [] raise NotImplementedError # for other drawables raise TypeError(f"Expected drawable objects or vector, but '{other.__class__.__name__}' found") def draw(self, screen, offset): """ Draw 'self' as an infinite ray on 'screen'. """ ray = Ray(self.start, self.offscreen(screen, offset)) # calculate real position on screen rect = Vector(*screen.get_rect()[2:]) for i, _ in enumerate(ray): ray[i] = round(ray[i] - offset + rect / 2) if ray.dir: # != 0 pygame.draw.aaline(screen, self.color, ray[0], ray[1])
from drawable import * from ray import * class Polygon(drawable): """ Polygon class as drawable object. Implements functionality of abstract drawable class. """ # constructor: create self as drawable object def __init__(self, *points, color=(0,0,0)): drawable.__init__(self, *points, color=color) # Implements draw() method of abstract drawable class def draw(self, screen, offset): if not self: # empty return polygon = self.copy() # move to real position on screen rect = Vector(*screen.get_rect()[2:]) for i, _ in enumerate(polygon): polygon[i] = round(polygon[i] - offset + rect / 2) if polygon[i].length2() > 30000: return if len(polygon) == 1: pygame.gfxdraw.pixel(screen, *polygon[0], self.color) elif len(polygon) == 2: pygame.draw.aaline(screen, self.color, polygon[0], polygon[1]) else: pygame.gfxdraw.aapolygon(screen, polygon, self.color) pygame.gfxdraw.filled_polygon(screen, polygon, self.color) # Implements draw_shadow() from abstract drawable class def draw_shadow(self, screen, offset, source, color=(31,31,31)): if len(self) == 0: return elif len(self) == 1: Ray(self[0], self[0] - source, color=color).draw(screen, offset) elif len(self) == 2: if not self[0].onscreen(screen, offset) and not self[1].onscreen(screen, offset): return if self[0] == source or self[1] == source: for point in self: Ray(point, point - source, color=color).draw(screen, offset) else: rect = screen.get_rect() corner = Vector(*rect[2:]) / 2 corners = [ corner * Vector( 1, 1), corner * Vector(-1, 1), corner * Vector(-1,-1), corner * Vector( 1,-1), corner * Vector( 1, 1), corner * Vector( 1,-1), ] ray1 = Ray(self[0], self[0] - source) ray2 = Ray(self[1], self[1] - source) if ray1.dir.angle_to(ray2.dir) > 180: ray1, ray2 = ray2, ray1 shadow = Polygon(color=color) shadow.append(ray2.offscreen(screen, offset)) shadow.append(ray2.start) shadow.append(ray1.start) shadow.append(ray1.offscreen(screen, offset)) for i, _ in enumerate(corners[1:-1]): if corners[i].is_between(ray1.dir, ray2.dir): if corners[i-1].is_between(ray1.dir, ray2.dir): shadow.append(offset + corners[i-1]) shadow.append(offset + corners[i]) if corners[i+1].is_between(ray1.dir, ray2.dir): shadow.append(offset + corners[i+1]) break shadow.draw(screen, offset) else: for i, _ in enumerate(self[:-1]): Polygon(self[i], self[i+1]).draw_shadow(screen, offset, source) Polygon(self[-1], self[0]).draw_shadow(screen, offset, source) # Implements dist_to() method of abstract drawable class def dist_to(self, source): def sdf_line(a, b, p): b_a = b - a p_a = p - a if b == a: h = 0 else: h = min(1, max(0, p_a.dot(b_a) / b_a.dot(b_a))) return (p_a - h * b_a).length() if len(self) == 0: return float('inf') if len(self) == 1: return (source - self[0]).length() else: m = float('inf') for i, _ in enumerate(self): m = min(m, sdf_line(self[i-1], self[i], source)) return m # Implements cut() from abstract drawable class def cut(self, source, portal): def position(point): vec = point - source if vec.is_between(ray1.dir, ray2.dir): if (point - ray1.start).is_between(ray1.dir, border.dir): return 1 else: return -1 if vec.is_between(null, ray1.dir): return 0 if vec.is_between(ray2.dir, null): return 2 ray1 = Ray(portal[0], portal[0] - source) ray2 = Ray(portal[1], portal[1] - source) border = Ray(ray1.start, ray2.start - ray1.start) if ray1.dir.angle_to(ray2.dir) > 180: ray1, ray2 = ray2, ray1 rest = Polygon() rest.__dict__ = self.__dict__ null = -(ray1.dir + ray2.dir) if len(self) == 0: return rest if len(self) == 2: if self[0] in portal and self[1] in portal: return rest nxt_pos = position(self[-1]) for i, _ in enumerate(self): cur_pos = nxt_pos nxt_pos = position(self[i]) if cur_pos == 1: rest.append(self[i-1]) if nxt_pos == cur_pos: continue inter = [] if 1 in (cur_pos, nxt_pos): if 0 in (cur_pos, nxt_pos): inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) elif 2 in (cur_pos, nxt_pos): inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) if not inter: # empty inter += border.intersect(Ray(self[i-1], self[i] - self[i-1])) elif -1 in (cur_pos, nxt_pos): if 0 in (cur_pos, nxt_pos): inter += [ray1.start] else: # 2 inter += [ray2.start] else: # 0 and 2 if cur_pos == 0: inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) else: # 2 inter += ray2.intersect(Ray(self[i-1], self[i] - self[i-1])) inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) for pt in inter: if pt.length2() > 10000: print(f"CalculationError while cutting {self} at {ray1, ray2}:\n {pt} is very big") else: rest.append(pt) if nxt_pos == 1: rest.append(self[-1]) return rest
import colors from polygon import * class Floor(Polygon): """ Class for polygons just drawn on the floor as drawable object. Implements functionality of abstract drawable class. """ # constructor: create self as drawable object def __init__(self, *points, color=colors.DARK_RED): Polygon.__init__(self, *points, color=color) # Implements draw_shadow() from abstract drawable class def draw_shadow(self, screen, offset, source, color=None): pass # Implements cut() from abstract drawable class def cut(self, source, portal): rest = Polygon.cut(self, source, portal) if len(rest) > 2: return Floor(*rest, color=self.color) return Polygon() def dist_to(self, source): return float('inf')
from polygon import * from matrix import * import colors class Portal(Polygon): """ Portal class as drawable object. Implements functionality of abstract drawable class. """ # constructor: create self as Polygon object def __init__(self, start, end, M=((1,0),(0,1)), M2=None, offset=None, offset2=None, color=(0,0,255)): Polygon.__init__(self, start, end, color=color) self.M = Matrix(*M) if M2 is None: self.M2 = self.M.inverse() else: self.M2 = Matrix(*M2) if offset is None: self.offset = None else: self.offset = Vector(*offset) if offset2 is None: self.offset2 = None else: self.offset2 = Vector(*offset2) # Overrides attribute access def __getattr__(self, attr): """ Return 'self[0]' and 'self[1]' as attribute 'start' and 'end'. """ if attr == 'start': return self[0] if attr == 'end': return self[1] if attr == 'M': return self.__dict__['M'] if attr == 'offset': return self.__dict__['offset'] if attr == 'color': return self.__dict__['color'] raise AttributeError(f"{self.__class__.__name__} has no Attribute {attr}.") def go_through(self, dir, world): portal_world = [] for object in world: port = object.__class__(*object) port.__dict__ = object.__dict__ if dir.is_between(self.end - self.start, self.start - self.end): for i, _ in enumerate(object): if self.offset is None: offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 else: offset = self.offset port[i] = self.M.prod(object[i]) - offset if type(object) is Portal: if self.M.i.angle_to(self.M.j) > 180: port[0], port[1] = port[1], port[0] else: for i, _ in enumerate(object): if self.offset2 is None: offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 else: offset2 = self.offset2 port[i] = self.M2.prod(object[i]) - offset2 if type(object) is Portal: if self.M2.i.angle_to(self.M2.j) > 180: port[0], port[1] = port[1], port[0] portal_world.append(port) return portal_world def portal_world(self, world, source): portal_world = [] for object in world: port = object.__class__(*object) port.__dict__ = object.__dict__ if (self.start - source).is_between(self.end - self.start, self.start - self.end): for i, _ in enumerate(object): if self.offset is None: offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 else: offset = self.offset port[i] = self.M.prod(object[i]) - offset if type(object) is Portal: if self.M.i.angle_to(self.M.j) > 180: port[0], port[1] = port[1], port[0] else: for i, _ in enumerate(object): if self.offset2 is None: offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 else: offset2 = self.offset2 port[i] = self.M2.prod(object[i]) - offset2 if type(object) is Portal: if self.M2.i.angle_to(self.M2.j) > 180: port[0], port[1] = port[1], port[0] cut = port.cut(source, self) if cut: # not empty portal_world.append(cut) return portal_world def draw(self, screen, offset): pass # overrides draw_shadow() inherited from Polygon class def draw_shadow(self, screen, offset, source): """ Draws shadow of portal Called by 'self.draw_shadow(screen, offset)' """ Polygon.draw_shadow(self, screen, offset, source, color=colors.GRAY) # Polygon.draw_shadow(self, screen, offset, source, color=(150,150,150)) for point in self: Ray(point, point - source, color=(0,0,0)).draw(screen, offset) # Implements cut() from abstract drawable class def cut(self, source, portal): rest = Polygon.cut(self, source, portal) if len(rest) >= 2: return Portal(*reversed(rest[:2]), M=self.M, color=self.color) return Polygon()