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
# NEG-World-Engine > A 2D Non-Euclidean Engine bases on portals ![](icon_large.png) **Dependencies:** - Python 3.7.7 or newer (get it [here](https://www.python.org/downloads/)) - PyGame 1.9.6 or newer (get it via `pip install pygame`) **Setup:** - Download the sourcecode ([here](https://gitlab.tubit.tu-berlin.de/srather/NEG-World-Engine.git)) - Unpack the downloaded archive - execute `main.py` **Controls:** - Move with WASD, the arrow keys or by clicking with your mouse - Press the number keys 0-9 to explore different worlds
A 2D Non-Euclidean Engine bases on portals
Dependencies:
pip install pygame
)Setup:
main.py
Controls:
import pygame import math import sys sys.path.append(".\\datatypes") sys.path.append(".\\drawables") sys.path.append(".\\utility") import generate import draw from vector import * from polygon import * from portal import * from floor import * # global variables WINDOW = Vector(1280, 960) POSITION = Vector(0, 0) ZOOM = 25 SPEED = 10 FPS = 70 DEPTH = 0 current_fps = FPS current_speed = Vector(0, 0) # generate worlds def gen4(): """ Generate world 4 (and 8). Return a big polygon with sides as mirrors and a slightly smaller one with polygons on its vertices. """ def mirror(m, v): v=Vector(*v) return 2 * v.dot(m) / m.dot(m) * m - v w = [] pol = generate.polygon(4, (0, 0), 500) for i, _ in enumerate(pol): w.append(generate.polygon(i+3, 0.5 * pol[i], 50, phase=0.5)) for i, _ in enumerate(pol): w.append(Portal(pol[i-1], pol[i], M=(mirror(pol[i]-pol[i-1],(1,0)), mirror(pol[i]-pol[i-1],(0,1))))) return w def gen6(): """ Generate additional objects for world 6. Return the 2 houses with their doors. """ w = [] w += generate.border(generate.polygon(4, (0, 0), 200, phase=-0.5))[:-1] w += generate.border(generate.polygon(4, (1000, 0), 200, phase=-0.5)) w += generate.door(Vector(0, -100*2**0.5), Vector(-100*2**0.5, -100*2**0.5), 0.5) for point in generate.polygon(4, (0, 0), 100*2**0.5): d = generate.door(point, Vector(0, 0), 0.5) w.append(d[0]); w.append(d[1]) for point in generate.polygon(4, (1000, 0), 100*2**0.5): d = generate.door(point, Vector(1000, 0), 0.5) w.append(d[0]); w.append(d[1]) return w worlds = [ # 1: short longer tunnel [ generate.polygon(3, (-150, -200), 50), Polygon(( 100,-100), (-100,-100)), Polygon(( 100,-200), (-100,-200)), Portal ((-100,-100), (-100,-200), M=((2.0,0), (0,1))), Portal (( 100,-200), ( 100,-100), M=((2.0,0), (0,1))), ], # 2: tilted tunnel [ Polygon(( 200, 100), (-200, 200)), Polygon(( 200, 200), (-200, 300)), Portal ((-200, 300), (-200, 200), M=((0.5,0.25), (0,1))), Portal (( 200, 100), ( 200, 200), M=((0.5,0.25), (0,1))), ], #3: 1 mirror [ generate.star(5, (200, -200), 50, phase=0.5), generate.polygon(4, (0, -200), 50), generate.polygon(7, (0, 800), 50, phase=0.5), Portal((-200, -300), (-200, -100), M=((-1,0),(0,1)), M2=((1,0),(0,1)), offset2=(0, 1000)), Portal((-200, 900), (-200, 700), M=((-1,0),(0,1)), M2=((1,0),(0,1)), offset2=(0, -1000)), ], # 4: 4 mirrors [ ] + gen4(), # 5: around the pole [ generate.polygon(3, (0, -200), 50, phase=0.5, color=colors.DARK_RED), generate.star(5, (2000, -200), 75, phase=0.5, color=colors.DARK_GREEN), Portal((0, -50), (1000, -50), offset=(2000, 0), offset2=(2000, 0)), Portal((2000, -50), (3000, -50), offset=(-2000, 0), offset2=(-2000, 0)), ], # 6: big room [ Polygon((100*2**0.5, -100*2**0.5), (0, -100*2**0.5)), Portal((0, 100*2**0.5), (0, 0), offset=(1000,0), offset2=(1000,0)), Portal((1000, 100*2**0.5), (1000, 0), offset=(-1000,0), offset2=(-1000,0)), Floor((*generate.polygon(4, ( -100/2**0.5, -100/2**0.5), 99.9, phase=0.5)), color=colors.DARK_RED), Floor((*generate.polygon(4, ( -100/2**0.5, 100/2**0.5), 99.9, phase=0.5)), color=colors.DARK_ORANGE), Floor((*generate.polygon(4, (1000+100/2**0.5, 100/2**0.5), 99.9, phase=0.5)), color=colors.DARK_YELLOW), Floor((*generate.polygon(4, (1000+100/2**0.5, -100/2**0.5), 99.9, phase=0.5)), color=colors.DARK_GREEN), Floor((*generate.polygon(4, (1000-100/2**0.5, -100/2**0.5), 99.9, phase=0.5)), color=colors.DARK_CYAN), Floor((*generate.polygon(4, (1000-100/2**0.5, 100/2**0.5), 99.9, phase=0.5)), color=colors.DARK_BLUE), Floor((*generate.polygon(4, ( 100/2**0.5, 100/2**0.5), 99.9, phase=0.5)), color=colors.DARK_PURPLE), Floor((*generate.polygon(4, ( 100/2**0.5, -100/2**0.5), 99.9, phase=0.5)), color=colors.DARK_MAGENTA), ] + gen6(), # 7: shorter/longer tunnel bad fps [ Polygon(( 100,-100), (-100,-100)), Polygon(( 100,-200), (-100,-200)), Portal ((-100,-100), (-100,-200), M=((2.0,0), (0,1))), Portal (( 100,-200), ( 100,-100), M=((2.0,0), (0,1))), Polygon(( 200, 100), (-200, 100)), Polygon(( 200, 200), (-200, 200)), Portal ((-200, 200), (-200, 100), M=((0.5,0), (0,1))), Portal (( 200, 100), ( 200, 200), M=((0.5,0), (0,1))), ], # 8: 4 mirors 2 depth [ ] + gen4(), # 9: shoter/longer tunnel depth: 0 [ Polygon(( 100,-100), (-100,-100)), Polygon(( 100,-200), (-100,-200)), Portal ((-100,-100), (-100,-200), M=((2.0,0), (0,1))), Portal (( 100,-200), ( 100,-100), M=((2.0,0), (0,1))), Polygon(( 200, 100), (-200, 100)), Polygon(( 200, 200), (-200, 200)), Portal ((-200, 200), (-200, 100), M=((0.5,0), (0,1))), Portal (( 200, 100), ( 200, 200), M=((0.5,0), (0,1))), ], # 0: random assortment [ Polygon((-30, 0)), Polygon((10, 30), (30, 100)), Polygon((100, -100), (200, -100)), Polygon((-100, -100), (-100, -200), (-200, -100)), Polygon((100, 100), (100, 200), (200, 200), (200, 100)), Portal((300, -200), (200, -300), M=((-1,0),(0,-1))), Portal((-100, 300), (100, 300), M=((1,0),(0,-0.5))), ], ] def bake_physics(wrld): """ Return a list of all lines an points in 'wrld' which have a collision. """ lns = [] for object in wrld: # ignore floor tiles if type(object) is Floor: continue # dissasample polygons into lines if isinstance(object, Polygon): if len(object) <= 2: lns.append(object) continue for i, _ in enumerate(object): lns.append(object.__class__(object[i-1], object[i])) return lns # initialise pygame.init() window = pygame.display.set_mode(WINDOW) pygame.display.set_caption("NEG World Engine") pygame.display.set_icon(pygame.image.load('icon_small.png')) clock = pygame.time.Clock() world = [] # empty starting world lines = bake_physics(world) # main loop running = True while running: # exit program for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # draw frame window.fill(colors.GRAY) draw.world(window, world, POSITION, DEPTH) draw.player(window, POSITION, ZOOM) draw.debug(window, 0, f"{current_fps:.0f} FPS", colors.BRIGHT_GREEN) draw.debug(window, 1, f"{POSITION.x:.0f} X {POSITION.y:.0f} y", colors.BRIGHT_GREEN) # check inputs keys = pygame.key.get_pressed() # change world if numbers are pressed if keys[pygame.K_1]: world = worlds[0]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 2 if keys[pygame.K_2]: world = worlds[1]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 2 if keys[pygame.K_3]: world = worlds[2]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 1 if keys[pygame.K_4]: world = worlds[3]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 1 if keys[pygame.K_5]: world = worlds[4]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 1 if keys[pygame.K_6]: world = worlds[5]; lines = bake_physics(world); POSITION = Vector(0, -200); current_speed *= 0; DEPTH = 1 if keys[pygame.K_7]: world = worlds[6]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 1 if keys[pygame.K_8]: world = worlds[7]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 2 if keys[pygame.K_9]: world = worlds[8]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 0 if keys[pygame.K_0]: world = worlds[9]; lines = bake_physics(world); POSITION *= 0; current_speed *= 0; DEPTH = 1 # change speed if arrows are pressed force = Vector(0, 0) if keys[pygame.K_LEFT ] or keys[pygame.K_a]: force += Vector(-1, 0) if keys[pygame.K_RIGHT] or keys[pygame.K_d]: force += Vector( 1, 0) if keys[pygame.K_UP ] or keys[pygame.K_w]: force += Vector( 0, -1) if keys[pygame.K_DOWN ] or keys[pygame.K_s]: force += Vector( 0, 1) force = force.normalised() # change speed if left mouse button is pressed if pygame.mouse.get_focused(): if pygame.mouse.get_pressed()[0]: force += (Vector(*pygame.mouse.get_pos()) - WINDOW / 2).normalised() # apply changes (not opimal, movement should be independant of framerate) current_speed += force.normalised() * SPEED / max(current_fps, 0.1) current_speed *= SPEED ** -(1 / max(current_fps, 60)) POSITION += current_speed # check physics (not optimal, player can glitch into things) for line in lines: if line.dist_to(POSITION) < ZOOM/2: if (POSITION - line[0]).length() < ZOOM/2: b_a = (POSITION - current_speed - line[0]).normal() elif (POSITION - line[1]).length() < ZOOM/2: b_a = (POSITION - current_speed - line[1]).normal() elif type(line) is Polygon: b_a = line[1] - line[0] else: continue # removes the part of the speed vector which causes collision POSITION -= current_speed current_speed = b_a * (b_a.dot(current_speed) / b_a.dot(b_a)) POSITION += current_speed # check if gone through portal move = Ray(POSITION - current_speed, current_speed) for object in world: if type(object) is Portal: # intersect portal with movement ray port = Ray(object[0], object[1] - object[0]) point = move.intersect(port) if point: # not empty if move.value_at(*point) <= 1 and port.value_at(*point) <= 1: # change world to portal world world = object.go_through(move.dir, world) lines = bake_physics(world) # wait tick and update clock.tick(FPS) current_fps = clock.get_fps() pygame.display.flip() # exit routine pygame.quit()