Hier werden die Unterschiede zwischen zwei Versionen gezeigt.
Beide Seiten der vorigen Revision Vorhergehende Überarbeitung Nächste Überarbeitung | Vorhergehende Überarbeitung | ||
ss20:neg_drawables [2020/08/26 21:04] srather [drawable] |
ss20:neg_drawables [2020/09/11 14:28] (aktuell) srather [Portal] |
||
---|---|---|---|
Zeile 4: | Zeile 4: | ||
====== Sourcecode ====== | ====== Sourcecode ====== | ||
- | Download sourcecode from Git repository: | + | Download the release version here:\\ |
+ | {{:ss20:neg-world-engine.zip}} | ||
- | [[https://gitlab.tubit.tu-berlin.de/srather/NEG-World-Engine.git|https://gitlab.tubit.tu-berlin.de/srather/NEG-World-Engine.git]] | + | Or download the newest version from the Git repository:\\ |
+ | [[https://gitlab.tubit.tu-berlin.de/srather/NEG-World-Engine.git]] | ||
==== Folder Structure: ==== | ==== Folder Structure: ==== | ||
Zeile 26: | Zeile 28: | ||
==== drawable ==== | ==== drawable ==== | ||
<file python drawable.py> | <file python drawable.py> | ||
+ | import colors | ||
from vector import * | from vector import * | ||
Zeile 31: | Zeile 34: | ||
class drawable(list): | class drawable(list): | ||
""" | """ | ||
- | Abstract superclass as a list of points for objects drawable on canvas. | + | Abstract superclass for all objects drawable on canvas. |
+ | A drawable is a list of points ('Vector') with a 'color' attribute. | ||
""" | """ | ||
- | # constructor: create self as list and populate it | + | def __init__(self, *points, color=colors.BLACK): |
- | def __init__(self, *points, color=(0,0,0)): | + | """ |
+ | Overrides '__init__' method of 'list' class. | ||
+ | Will only accept point-like objects and adds 'color' attribute. | ||
+ | """ | ||
list.__init__(self) | list.__init__(self) | ||
for point in points: | for point in points: | ||
Zeile 43: | Zeile 51: | ||
- | # override append() inherited from list to only accept points | ||
def append(self, item): | def append(self, item): | ||
+ | """ | ||
+ | Overrides 'append' method of 'list' class. | ||
+ | Will only accept point-like objects. | ||
+ | """ | ||
+ | |||
try: | try: | ||
list.append(self, Vector(*item)) | list.append(self, Vector(*item)) | ||
Zeile 51: | Zeile 63: | ||
- | # override __str__() inherited from list to display class and instance variables | + | def __str__(self): |
- | def __str__(self): # sorry this is a mess | + | """ |
+ | 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(' ', '')}" | return f"{self.__class__.__name__}({list.__repr__(self)[1:-1]}) with {self.__dict__.__repr__()[1:-1].replace(':', ' =').replace(' ', '')}" | ||
- | # override __repr__() inherited from list to display class and instance variables | + | def __repr__(self): |
- | def __repr__(self): # sorry this is a mess | + | """ |
+ | 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(' ', '')})" | return f"{self.__class__.__name__}({list.__repr__(self)[1:-1]}, {self.__dict__.__repr__()[1:-1].replace(':', ' =').replace(' ', '')})" | ||
- | # these have to be implemented by a subclass | ||
def draw(self, screen, offset): | def draw(self, screen, offset): | ||
+ | """Abstract method to draw 'self' on 'screen'.""" | ||
raise NotImplementedError | raise NotImplementedError | ||
- | def draw_shadow(self, screen, source): | + | def draw_shadow(self, screen, offset, source, color=(31,31,31)): |
+ | """Abstract method to draw a shadow of 'self' on 'screen'.""" | ||
raise NotImplementedError | raise NotImplementedError | ||
- | def dist_to(self, other): # used for colision detection | + | def dist_to(self, source): |
+ | """Abstract method to calculate the distance to the player ('source').""" | ||
raise NotImplementedError | raise NotImplementedError | ||
- | def intersect(self, other): # returns list of points | + | def cut(self, source, portal): |
- | raise NotImplementedError | + | """Abstract method to cut 'self' to fit in the portal view.""" |
- | + | ||
- | + | ||
- | def cut(self, ray): # returns part of object | + | |
raise NotImplementedError | raise NotImplementedError | ||
</file> | </file> | ||
- | [[neg_drawables#sourcecode|↑ Back to top]] | + | [[#top|↑ Back to top]] |
---- | ---- | ||
Zeile 91: | Zeile 110: | ||
<file python ray.py> | <file python ray.py> | ||
import pygame.gfxdraw | import pygame.gfxdraw | ||
- | import numpy | + | import colors |
from drawable import * | from drawable import * | ||
from matrix import * | from matrix import * | ||
Zeile 98: | Zeile 117: | ||
class Ray(drawable): | class Ray(drawable): | ||
""" | """ | ||
- | Ray class as drawable object for visual debug. A Ray is a vector with | + | Ray class as drawable object for visual debug. |
- | a starting point, visualised as a line with a startpoint, but no end. | + | A Ray has a starting point and a direction vector. |
- | + | ||
- | Create a new 'Ray' object using 'ray = Ray((x1, y1), (x2, y2))' | + | |
- | or if the points are in a list like '[(x1, y1), (x2, y2)]' | + | |
- | use 'ray = Ray(*[(x1, y1), (x2, y2)])' to unpack it. | + | |
- | Note that 'Ray(*ray)' creates a copy of 'ray'. | + | |
""" | """ | ||
- | def __init__(self, start, direction, color=(255,0,0)): | + | def __init__(self, start, dir, color=colors.BRIGHT_RED): |
- | """Call the init function of parent class 'drawable'.""" | + | """Initilise 'self' with standard color 'BRIGHT_RED'.""" |
- | drawable.__init__(self, start, direction, color=color) | + | drawable.__init__(self, start, dir, color=color) |
- | # Overrides attribute access | ||
def __getattr__(self, attr): | def __getattr__(self, attr): | ||
""" | """ | ||
Zeile 123: | Zeile 136: | ||
if attr == 'dir': | if attr == 'dir': | ||
return self[1] | return self[1] | ||
- | if attr == 'color': | ||
- | return self.__dict__['color'] | ||
- | |||
- | raise AttributeError(f"{self.__class__.__name__} has no Attribute {attr}.") | ||
def point_at(self, value): | def point_at(self, value): | ||
""" | """ | ||
- | Return the point 'value' times 'self.dir' from 'self.start'. | + | Return the point 'self.start + value * self.dir'. |
""" | """ | ||
Zeile 142: | Zeile 151: | ||
def value_at(self, point): | def value_at(self, point): | ||
""" | """ | ||
- | Return the how many 'self.dir' 'point' is from 'self.start'. | + | Return the value with 'point_at(value) == point'. |
""" | """ | ||
Zeile 151: | Zeile 160: | ||
return value | return value | ||
+ | # hotfix | ||
h = (point - self.start).dot(self.dir) / self.dir.dot(self.dir) | 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 | return h | ||
- | |||
- | print(f"Warning: returned 'None' for value_at of point {((point - self.start) - h * self.dir).length()} off the ray)") | ||
- | return None | ||
raise TypeError(f"Expected Vector, but {point.__class__.__name__} found") | raise TypeError(f"Expected Vector, but {point.__class__.__name__} found") | ||
Zeile 162: | Zeile 170: | ||
def offscreen(self, screen, offset): | def offscreen(self, screen, offset): | ||
""" | """ | ||
- | Return a point of the ray that is definetly not on 'screen'. | + | Return a point of the ray that is not on the rect 'screen'. |
+ | This assumes all rays go away from the 'screen' center. | ||
""" | """ | ||
Zeile 183: | Zeile 192: | ||
- | # Implements intersect() from abstract drawable class | ||
def intersect(self, other): | def intersect(self, other): | ||
""" | """ | ||
Zeile 192: | Zeile 200: | ||
return [] | return [] | ||
+ | # point | ||
if isinstance(other, Vector): | if isinstance(other, Vector): | ||
if self.dir.multiple_of(other - self.start): # point on self | if self.dir.multiple_of(other - self.start): # point on self | ||
if self.value_at(other) is not None: | if self.value_at(other) is not None: | ||
return [other] | return [other] | ||
- | |||
return [] | return [] | ||
Zeile 204: | Zeile 212: | ||
return self.intersect(other.start) | return self.intersect(other.start) | ||
- | if self.dir.x == 0: # switch vectors for gauss algorithm | + | if self.dir.x == 0: |
+ | # switch vectors for gauss algorithm | ||
return other.intersect(self) | return other.intersect(self) | ||
Zeile 223: | Zeile 232: | ||
return [self.point_at(matrix[0][2])] | return [self.point_at(matrix[0][2])] | ||
return [] | return [] | ||
- | |||
- | |||
- | if type(other) is Polygon: | ||
- | if len(other) == 1: | ||
- | return self.intersect(*other) | ||
- | |||
- | if len(other) >= 2: | ||
- | lines = [] | ||
- | for i, point in other[:-1]: | ||
- | lines.append(Ray(other[i], other[i+1] - other[i])) | ||
- | if len(other) >= 3: | ||
- | lines.append(Ray(other[-1], other[0] - other[-1])) | ||
- | |||
- | points = [] | ||
- | for line in lines: | ||
- | point = self.intersect(line) | ||
- | if len(point) == 1 and point[0] / line.dir <= 1: | ||
- | points += point | ||
- | |||
- | values = [] | ||
- | for point in points: | ||
- | values.append(self.value_at(point)) | ||
- | values.sort() | ||
- | |||
- | intersections = [] | ||
- | for value in values: | ||
- | intersections.append(self.point_at(value)) | ||
- | |||
- | return intersections | ||
- | |||
- | return [] # for empty polygons | ||
raise NotImplementedError # for other drawables | raise NotImplementedError # for other drawables | ||
- | raise TypeError(f"Expected drawable objects, but '{other.__class__.__name__}' found") | + | raise TypeError(f"Expected drawable objects or vector, but '{other.__class__.__name__}' found") |
- | # Implements draw() method of abstract drawable class | ||
def draw(self, screen, offset): | def draw(self, screen, offset): | ||
""" | """ | ||
- | Visualizes 'self' as a line with a startpoint, but no end. | + | Draw 'self' as an infinite ray on 'screen'. |
""" | """ | ||
ray = Ray(self.start, self.offscreen(screen, offset)) | ray = Ray(self.start, self.offscreen(screen, offset)) | ||
- | # move to real position on screen | + | # calculate real position on screen |
rect = Vector(*screen.get_rect()[2:]) | rect = Vector(*screen.get_rect()[2:]) | ||
for i, _ in enumerate(ray): | for i, _ in enumerate(ray): | ||
Zeile 278: | Zeile 255: | ||
</file> | </file> | ||
- | [[neg_drawables#sourcecode|↑ Back to top]] | + | [[#top|↑ Back to top]] |
---- | ---- | ||
Zeile 285: | Zeile 262: | ||
==== Polygon ==== | ==== Polygon ==== | ||
<file python polygon.py> | <file python polygon.py> | ||
+ | import colors | ||
from drawable import * | from drawable import * | ||
from ray import * | from ray import * | ||
Zeile 291: | Zeile 269: | ||
class Polygon(drawable): | class Polygon(drawable): | ||
""" | """ | ||
- | Polygon class as drawable object. Implements functionality of abstract drawable class. | + | Polygon class as 'drawable' objects. |
+ | Implements functionality of abstract drawable class. | ||
""" | """ | ||
- | # constructor: create self as drawable object | + | def __init__(self, *points, color=colors.BLACK): |
- | def __init__(self, *points, color=(0,0,0)): | + | """Initilise 'self' with standard color 'BLACK'.""" |
drawable.__init__(self, *points, color=color) | drawable.__init__(self, *points, color=color) | ||
- | # Implements draw() method of abstract drawable class | ||
def draw(self, screen, offset): | def draw(self, screen, offset): | ||
+ | """ | ||
+ | Draw 'self' as a polygon on 'screen'. | ||
+ | """ | ||
+ | |||
if not self: # empty | if not self: # empty | ||
return | return | ||
Zeile 307: | Zeile 289: | ||
polygon = self.copy() | polygon = self.copy() | ||
- | # move to real position on screen | + | # calculate real position on screen |
rect = Vector(*screen.get_rect()[2:]) | rect = Vector(*screen.get_rect()[2:]) | ||
for i, _ in enumerate(polygon): | for i, _ in enumerate(polygon): | ||
polygon[i] = round(polygon[i] - offset + rect / 2) | polygon[i] = round(polygon[i] - offset + rect / 2) | ||
- | if polygon[i].length2() > 30000: | + | if polygon[i].length2() > 30000: # hotfix |
return | return | ||
+ | # point | ||
if len(polygon) == 1: | if len(polygon) == 1: | ||
pygame.gfxdraw.pixel(screen, *polygon[0], self.color) | pygame.gfxdraw.pixel(screen, *polygon[0], self.color) | ||
+ | # line | ||
elif len(polygon) == 2: | elif len(polygon) == 2: | ||
pygame.draw.aaline(screen, self.color, polygon[0], polygon[1]) | pygame.draw.aaline(screen, self.color, polygon[0], polygon[1]) | ||
+ | # polygon | ||
else: | else: | ||
pygame.gfxdraw.aapolygon(screen, polygon, self.color) | pygame.gfxdraw.aapolygon(screen, polygon, self.color) | ||
Zeile 323: | Zeile 308: | ||
- | # Implements draw_shadow() from abstract drawable class | + | def draw_shadow(self, screen, offset, source, color=colors.DARK_GRAY): |
- | def draw_shadow(self, screen, offset, source, color=(31,31,31)): | + | """ |
- | if len(self) == 0: | + | Draw draw a shadow of 'self' on 'screen' respecting the light-'source'. |
+ | """ | ||
+ | |||
+ | if not self: # empty | ||
return | return | ||
+ | # point | ||
elif len(self) == 1: | elif len(self) == 1: | ||
Ray(self[0], self[0] - source, color=color).draw(screen, offset) | Ray(self[0], self[0] - source, color=color).draw(screen, offset) | ||
+ | # line | ||
elif len(self) == 2: | elif len(self) == 2: | ||
- | if not self[0].onscreen(screen, offset) and not self[1].onscreen(screen, offset): | + | # edgecase: would lead to DivisionByZeroError |
- | return | + | |
if self[0] == source or self[1] == source: | if self[0] == source or self[1] == source: | ||
for point in self: | for point in self: | ||
Ray(point, point - source, color=color).draw(screen, offset) | Ray(point, point - source, color=color).draw(screen, offset) | ||
- | else: | + | return |
- | 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) | + | # corners of screen area |
- | ray2 = Ray(self[1], self[1] - source) | + | rect = screen.get_rect() |
- | if ray1.dir.angle_to(ray2.dir) > 180: | + | corner = Vector(*rect[2:]) / 2 |
- | ray1, ray2 = ray2, ray1 | + | 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), | ||
+ | ] | ||
+ | # rays from source to both ends | ||
+ | ray1 = Ray(self[0], self[0] - source) | ||
+ | ray2 = Ray(self[1], self[1] - source) | ||
+ | # garantees anticlockwise order (important for calculations) | ||
+ | if ray1.dir.angle_to(ray2.dir) > 180: | ||
+ | ray1, ray2 = ray2, ray1 | ||
- | shadow = Polygon(color=color) | + | # creates minimal polygon |
- | shadow.append(ray2.offscreen(screen, offset)) | + | shadow = Polygon(color=color) |
- | shadow.append(ray2.start) | + | shadow.append(ray2.offscreen(screen, offset)) |
- | shadow.append(ray1.start) | + | shadow.append(ray2.start) |
- | shadow.append(ray1.offscreen(screen, offset)) | + | shadow.append(ray1.start) |
+ | shadow.append(ray1.offscreen(screen, offset)) | ||
- | for i, _ in enumerate(corners[1:-1]): | + | # adds corners of screen to polygon if needed |
- | if corners[i].is_between(ray1.dir, ray2.dir): | + | for i, _ in enumerate(corners[1:-1]): |
- | if corners[i-1].is_between(ray1.dir, ray2.dir): | + | if corners[i].is_between(ray1.dir, ray2.dir): |
- | shadow.append(offset + corners[i-1]) | + | if corners[i-1].is_between(ray1.dir, ray2.dir): |
- | shadow.append(offset + corners[i]) | + | shadow.append(offset + corners[i-1]) |
- | if corners[i+1].is_between(ray1.dir, ray2.dir): | + | shadow.append(offset + corners[i]) |
- | shadow.append(offset + corners[i+1]) | + | if corners[i+1].is_between(ray1.dir, ray2.dir): |
- | break | + | shadow.append(offset + corners[i+1]) |
+ | break | ||
- | shadow.draw(screen, offset) | + | # finally draws calculated shadow |
+ | shadow.draw(screen, offset) | ||
- | else: | + | # polygon |
+ | else: # draws shadow for every line in polygon | ||
for i, _ in enumerate(self[:-1]): | for i, _ in enumerate(self[:-1]): | ||
Polygon(self[i], self[i+1]).draw_shadow(screen, offset, source) | Polygon(self[i], self[i+1]).draw_shadow(screen, offset, source) | ||
Zeile 378: | Zeile 373: | ||
- | # Implements dist_to() method of abstract drawable class | ||
def dist_to(self, source): | def dist_to(self, source): | ||
+ | """ | ||
+ | Return euclidean distance to the player ('source'). | ||
+ | """ | ||
+ | |||
def sdf_line(a, b, p): | def sdf_line(a, b, p): | ||
- | b_a = b - a | + | """ |
- | p_a = p - a | + | Return distance from point p to line a-b. |
+ | (source: https://youtu.be/PMltMdi1Wzg) | ||
+ | """ | ||
if b == a: | if b == a: | ||
h = 0 | h = 0 | ||
else: | else: | ||
- | h = min(1, max(0, p_a.dot(b_a) / b_a.dot(b_a))) | + | h = min(1, max(0, (p-a).dot(b-a) / (b-a).dot(b-a))) |
- | return (p_a - h * b_a).length() | + | return ((p-a) - h * (b-a)).length() |
- | if len(self) == 0: | + | if not self: # empty |
return float('inf') | return float('inf') | ||
- | if len(self) == 1: | + | # minimum distance to a side |
- | return (source - self[0]).length() | + | m = float('inf') |
- | + | for i, _ in enumerate(self): | |
- | else: | + | m = min(m, sdf_line(self[i-1], self[i], source)) |
- | m = float('inf') | + | return m |
- | 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 cut(self, source, portal): | ||
+ | """ | ||
+ | Return 'self' but cut to fit in the view from 'source' though 'portal'. | ||
+ | This algorithm isn't perfect, but the best I got at the moment. | ||
+ | """ | ||
+ | |||
def position(point): | def position(point): | ||
+ | """ | ||
+ | Return the area 'point' is in: | ||
+ | -1 -- in the triangle between source and portal line | ||
+ | 0 -- to the right of visible area | ||
+ | 1 -- in the visible portal area | ||
+ | 2 -- to the left of visible area | ||
+ | """ | ||
+ | |||
vec = point - source | vec = point - source | ||
if vec.is_between(ray1.dir, ray2.dir): | if vec.is_between(ray1.dir, ray2.dir): | ||
Zeile 411: | Zeile 421: | ||
else: | else: | ||
return -1 | return -1 | ||
- | |||
if vec.is_between(null, ray1.dir): | if vec.is_between(null, ray1.dir): | ||
return 0 | return 0 | ||
Zeile 417: | Zeile 426: | ||
return 2 | return 2 | ||
- | ray1 = Ray(portal[0], portal[0] - source) | + | # rays that are borders of different areas |
- | ray2 = Ray(portal[1], portal[1] - source) | + | ray1 = Ray(portal.start, portal.start - source) # area 0 <--> 1 |
- | border = Ray(ray1.start, ray2.start - ray1.start) | + | ray2 = Ray(portal.end, portal.end - source) # area 1 <--> 2 |
+ | border = Ray(ray1.start, ray2.start - ray1.start) # area -1 <--> 1 | ||
+ | null = -(ray1.dir + ray2.dir) # area 0 <--> 2 | ||
+ | # garantees anticlockwise order (important for calculations) | ||
if ray1.dir.angle_to(ray2.dir) > 180: | if ray1.dir.angle_to(ray2.dir) > 180: | ||
ray1, ray2 = ray2, ray1 | ray1, ray2 = ray2, ray1 | ||
+ | |||
rest = Polygon() | rest = Polygon() | ||
rest.__dict__ = self.__dict__ | rest.__dict__ = self.__dict__ | ||
- | null = -(ray1.dir + ray2.dir) | ||
- | if len(self) == 0: | + | if not self: # empty |
return rest | return rest | ||
+ | # edgecase for when a portal is in its own portal world in the same place | ||
if len(self) == 2: | if len(self) == 2: | ||
if self[0] in portal and self[1] in portal: | if self[0] in portal and self[1] in portal: | ||
return rest | return rest | ||
+ | # algorithm for cutting polygon | ||
nxt_pos = position(self[-1]) | nxt_pos = position(self[-1]) | ||
for i, _ in enumerate(self): | for i, _ in enumerate(self): | ||
Zeile 438: | Zeile 452: | ||
nxt_pos = position(self[i]) | nxt_pos = position(self[i]) | ||
- | if cur_pos == 1: | + | if cur_pos == 1: # visible |
rest.append(self[i-1]) | rest.append(self[i-1]) | ||
+ | # same position, nothing changes | ||
if nxt_pos == cur_pos: | if nxt_pos == cur_pos: | ||
continue | continue | ||
+ | # different position, intersections with visible area needed | ||
inter = [] | inter = [] | ||
+ | # through the left or right boundary of visible area -> intersection | ||
if 1 in (cur_pos, nxt_pos): | if 1 in (cur_pos, nxt_pos): | ||
if 0 in (cur_pos, nxt_pos): | if 0 in (cur_pos, nxt_pos): | ||
Zeile 454: | Zeile 471: | ||
inter += border.intersect(Ray(self[i-1], self[i] - self[i-1])) | inter += border.intersect(Ray(self[i-1], self[i] - self[i-1])) | ||
+ | # line goes outside the visible area around the portal -> edges of portal | ||
elif -1 in (cur_pos, nxt_pos): | elif -1 in (cur_pos, nxt_pos): | ||
if 0 in (cur_pos, nxt_pos): | if 0 in (cur_pos, nxt_pos): | ||
Zeile 460: | Zeile 478: | ||
inter += [ray2.start] | inter += [ray2.start] | ||
+ | # line goes from the left to the right -> may intersect visible area | ||
else: # 0 and 2 | else: # 0 and 2 | ||
if cur_pos == 0: | if cur_pos == 0: | ||
Zeile 468: | Zeile 487: | ||
inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) | inter += ray1.intersect(Ray(self[i-1], self[i] - self[i-1])) | ||
+ | # add intersections | ||
for pt in inter: | for pt in inter: | ||
- | if pt.length2() > 10000: | + | if pt.length2() > 10000: # hotfix |
print(f"CalculationError while cutting {self} at {ray1, ray2}:\n {pt} is very big") | print(f"CalculationError while cutting {self} at {ray1, ray2}:\n {pt} is very big") | ||
else: | else: | ||
rest.append(pt) | rest.append(pt) | ||
- | |||
- | if nxt_pos == 1: | ||
- | rest.append(self[-1]) | ||
return rest | return rest | ||
Zeile 481: | Zeile 498: | ||
</file> | </file> | ||
- | [[neg_drawables#sourcecode|↑ Back to top]] | + | [[#top|↑ Back to top]] |
---- | ---- | ||
Zeile 490: | Zeile 507: | ||
import colors | import colors | ||
from polygon import * | from polygon import * | ||
+ | |||
class Floor(Polygon): | class Floor(Polygon): | ||
""" | """ | ||
- | Class for polygons just drawn on the floor as drawable object. Implements functionality of abstract drawable class. | + | Subclass for polygons as floor tiles wich don't have collisions. |
""" | """ | ||
- | # constructor: create self as drawable object | ||
def __init__(self, *points, color=colors.DARK_RED): | def __init__(self, *points, color=colors.DARK_RED): | ||
+ | """Initilise 'self' with standard color 'DARK_RED'.""" | ||
Polygon.__init__(self, *points, color=color) | Polygon.__init__(self, *points, color=color) | ||
- | # Implements draw_shadow() from abstract drawable class | ||
def draw_shadow(self, screen, offset, source, color=None): | def draw_shadow(self, screen, offset, source, color=None): | ||
+ | """Overide 'draw_shadow' to not draw a shadow.""" | ||
pass | pass | ||
- | # Implements cut() from abstract drawable class | ||
def cut(self, source, portal): | def cut(self, source, portal): | ||
+ | """ | ||
+ | Return 'self' but cut to fit in the view from 'source' though 'portal'. | ||
+ | Makes use of 'Polygon.cut'. | ||
+ | """ | ||
+ | |||
rest = Polygon.cut(self, source, portal) | rest = Polygon.cut(self, source, portal) | ||
if len(rest) > 2: | if len(rest) > 2: | ||
return Floor(*rest, color=self.color) | return Floor(*rest, color=self.color) | ||
- | + | else: | |
- | return Polygon() | + | return Polygon() |
def dist_to(self, source): | def dist_to(self, source): | ||
+ | """Overide 'dist_to' to never cause collisions.""" | ||
return float('inf') | return float('inf') | ||
</file> | </file> | ||
- | [[neg_drawables#sourcecode|↑ Back to top]] | + | [[#top|↑ Back to top]] |
---- | ---- | ||
Zeile 528: | Zeile 551: | ||
==== Portal ==== | ==== Portal ==== | ||
<file python portal.py> | <file python portal.py> | ||
+ | import colors | ||
from polygon import * | from polygon import * | ||
from matrix import * | from matrix import * | ||
- | import colors | ||
class Portal(Polygon): | class Portal(Polygon): | ||
""" | """ | ||
- | Portal class as drawable object. Implements functionality of abstract drawable class. | + | Portal class as a 2-element 'Polygon' object. |
+ | In for both sides of the portal there is | ||
+ | an offset vector and a distortion matrix. | ||
""" | """ | ||
# constructor: create self as Polygon object | # 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)): | + | def __init__(self, start, end, M=((1,0),(0,1)), M2=None, offset=None, offset2=None, color=colors.BRIGHT_BLUE): |
+ | """ | ||
+ | Initilises 'self' with standard color 'BRIGHT_BLUE'. | ||
+ | offset -- for side 1 (std: None == portal will remain in same place) | ||
+ | offset2 -- for side 2 (std: None == portal will remain in same place) | ||
+ | M -- distortion matrix for side 1 (std: unit matrix) | ||
+ | M2 -- distortion matrix for side 2 (std: None == inverse of M) | ||
+ | """ | ||
Polygon.__init__(self, start, end, color=color) | Polygon.__init__(self, start, end, color=color) | ||
+ | |||
self.M = Matrix(*M) | self.M = Matrix(*M) | ||
if M2 is None: | if M2 is None: | ||
Zeile 557: | Zeile 591: | ||
- | # Overrides attribute access | ||
def __getattr__(self, attr): | def __getattr__(self, attr): | ||
""" | """ | ||
Zeile 567: | Zeile 600: | ||
if attr == 'end': | if attr == 'end': | ||
return self[1] | 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): | def go_through(self, dir, world): | ||
+ | """ | ||
+ | Return the portal world of 'world' for the side in direction 'dir'. | ||
+ | """ | ||
+ | |||
portal_world = [] | portal_world = [] | ||
Zeile 584: | Zeile 613: | ||
port.__dict__ = object.__dict__ | port.__dict__ = object.__dict__ | ||
+ | # gone through side 1 | ||
if dir.is_between(self.end - self.start, self.start - self.end): | if dir.is_between(self.end - self.start, self.start - self.end): | ||
for i, _ in enumerate(object): | for i, _ in enumerate(object): | ||
if self.offset is None: | if self.offset is None: | ||
+ | # middle of portal in portal world | ||
offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | ||
else: | else: | ||
offset = self.offset | offset = self.offset | ||
+ | # apply distortion and offset | ||
port[i] = self.M.prod(object[i]) - offset | port[i] = self.M.prod(object[i]) - offset | ||
+ | # if world gets mirrored the side-sensitive portals have to be reverted | ||
if type(object) is Portal: | if type(object) is Portal: | ||
if self.M.i.angle_to(self.M.j) > 180: | if self.M.i.angle_to(self.M.j) > 180: | ||
port[0], port[1] = port[1], port[0] | port[0], port[1] = port[1], port[0] | ||
+ | |||
+ | # gone through side 2 | ||
else: | else: | ||
for i, _ in enumerate(object): | for i, _ in enumerate(object): | ||
if self.offset2 is None: | if self.offset2 is None: | ||
+ | # middle of portal in portal world | ||
offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | ||
else: | else: | ||
offset2 = self.offset2 | offset2 = self.offset2 | ||
+ | # apply distortion and offset | ||
port[i] = self.M2.prod(object[i]) - offset2 | port[i] = self.M2.prod(object[i]) - offset2 | ||
+ | # if world gets mirrored the side-sensitive portals have to be reverted | ||
if type(object) is Portal: | if type(object) is Portal: | ||
if self.M2.i.angle_to(self.M2.j) > 180: | if self.M2.i.angle_to(self.M2.j) > 180: | ||
port[0], port[1] = port[1], port[0] | port[0], port[1] = port[1], port[0] | ||
+ | # add distorted object to portal world | ||
portal_world.append(port) | portal_world.append(port) | ||
Zeile 621: | Zeile 660: | ||
port.__dict__ = object.__dict__ | port.__dict__ = object.__dict__ | ||
+ | # gone through side 1 | ||
if (self.start - source).is_between(self.end - self.start, self.start - self.end): | if (self.start - source).is_between(self.end - self.start, self.start - self.end): | ||
for i, _ in enumerate(object): | for i, _ in enumerate(object): | ||
if self.offset is None: | if self.offset is None: | ||
+ | # middle of portal in portal world | ||
offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | offset = self.M.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | ||
else: | else: | ||
offset = self.offset | offset = self.offset | ||
+ | # apply distortion and offset | ||
port[i] = self.M.prod(object[i]) - offset | port[i] = self.M.prod(object[i]) - offset | ||
+ | # if world gets mirrored the side-sensitive portals have to be reverted | ||
if type(object) is Portal: | if type(object) is Portal: | ||
if self.M.i.angle_to(self.M.j) > 180: | if self.M.i.angle_to(self.M.j) > 180: | ||
port[0], port[1] = port[1], port[0] | port[0], port[1] = port[1], port[0] | ||
+ | |||
+ | # gone through side 2 | ||
else: | else: | ||
for i, _ in enumerate(object): | for i, _ in enumerate(object): | ||
if self.offset2 is None: | if self.offset2 is None: | ||
+ | # middle of portal in portal world | ||
offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | offset2 = self.M2.prod((self.start + self.end) / 2) - (self.start + self.end) / 2 | ||
else: | else: | ||
offset2 = self.offset2 | offset2 = self.offset2 | ||
+ | # apply distortion and offset | ||
port[i] = self.M2.prod(object[i]) - offset2 | port[i] = self.M2.prod(object[i]) - offset2 | ||
+ | # if world gets mirrored the side-sensitive portals have to be reverted | ||
if type(object) is Portal: | if type(object) is Portal: | ||
if self.M2.i.angle_to(self.M2.j) > 180: | if self.M2.i.angle_to(self.M2.j) > 180: | ||
port[0], port[1] = port[1], port[0] | port[0], port[1] = port[1], port[0] | ||
+ | # add distorted and truncated object to portal world | ||
cut = port.cut(source, self) | cut = port.cut(source, self) | ||
if cut: # not empty | if cut: # not empty | ||
Zeile 654: | Zeile 703: | ||
def draw(self, screen, offset): | def draw(self, screen, offset): | ||
- | pass | + | """ |
+ | Draw the endpoints of 'self' on 'screen'. | ||
+ | """ | ||
+ | |||
+ | for point in self: | ||
+ | Polygon(point).draw(screen, offset) | ||
- | # overrides draw_shadow() inherited from Polygon class | ||
def draw_shadow(self, screen, offset, source): | def draw_shadow(self, screen, offset, source): | ||
""" | """ | ||
- | Draws shadow of portal | + | Draw draw a shadow of 'self' on 'screen' respecting the light-'source'. |
- | Called by 'self.draw_shadow(screen, offset)' | + | |
""" | """ | ||
+ | # draw the shadow in the same color as the background to wipe the canvas | ||
Polygon.draw_shadow(self, screen, offset, source, color=colors.GRAY) | Polygon.draw_shadow(self, screen, offset, source, color=colors.GRAY) | ||
- | # Polygon.draw_shadow(self, screen, offset, source, color=(150,150,150)) | ||
+ | # draw shadows for the end points of portal | ||
for point in self: | for point in self: | ||
- | Ray(point, point - source, color=(0,0,0)).draw(screen, offset) | + | Polygon(point).draw_shadow(screen, offset, source) |
- | # Implements cut() from abstract drawable class | ||
def cut(self, source, portal): | def cut(self, source, portal): | ||
+ | """ | ||
+ | Return 'self' but cut to fit in the view from 'source' though 'portal'. | ||
+ | Makes use of 'Polygon.cut'. | ||
+ | """ | ||
+ | |||
rest = Polygon.cut(self, source, portal) | rest = Polygon.cut(self, source, portal) | ||
- | if len(rest) >= 2: | + | if len(rest) == 2: |
return Portal(*reversed(rest[:2]), M=self.M, color=self.color) | return Portal(*reversed(rest[:2]), M=self.M, color=self.color) | ||
Zeile 680: | Zeile 738: | ||
</file> | </file> | ||
- | [[neg_drawables#sourcecode|↑ Back to top]] | + | [[#top|↑ Back to top]] |