Skip to content

Commit

Permalink
Added some sort of example. Asteroids. Still needs work on world coll…
Browse files Browse the repository at this point in the history
…ision API...
  • Loading branch information
taras-doba-ua committed May 9, 2015
1 parent afe42c4 commit 3e88ba1
Show file tree
Hide file tree
Showing 7 changed files with 732 additions and 1 deletion.
3 changes: 2 additions & 1 deletion gengine/collision/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from .shapes import Circle, BoundingBox, Polygon
from .shapes import Circle, BoundingBox, Polygon, Shape
from .intersection import intersects
from .containment import contains
from .quadtree import QuadTree


__all__ = [
"Shape",
"Circle",
"BoundingBox",
"intersects",
Expand Down
191 changes: 191 additions & 0 deletions gengine/examples/asteroids/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import abc
import math
from planar import Vec2, Affine
from pyglet.gl import GL_LINE_LOOP
from pyglet.graphics import draw as gl_draw

from gengine.collision import Shape, Polygon, Circle


class Component:
__metaclass__ = abc.ABCMeta

_world = None

def __init__(
self,
position: Vec2,
shape: Shape,
timestamp: int):
self._position = position
self._shape = shape
self._timestamp = timestamp

@property
def shape(self):
return self._shape

@property
def position(self):
return self._position

@property
def timestamp(self):
return self._timestamp


class PolygonComponent(Component):

def draw(self, timestamp):
dt = timestamp - self.timestamp
shape = self.shape
transform = self._get_transform(dt)
n_points = len(shape)
flatten_array = []
for vertice in shape:
flatten_array.extend(vertice * transform)
gl_draw(n_points, GL_LINE_LOOP, ('v2f', flatten_array))

def _get_transform(self, dt):
position = Affine.translation(self._position + self._velocity * dt)
return position


class Asteroid(PolygonComponent):

ROTATION_SPEED = 60 # deg/s

def __init__(
self,
position: Vec2,
velocity: Vec2,
timestamp: int):
shape = Polygon([
Vec2(2, 4), Vec2(4, 2),
Vec2(2, 1), Vec2(5, -1),
Vec2(2, -4), Vec2(-2, -4),
Vec2(-4, -2), Vec2(-4, 2),
Vec2(-2, 4), Vec2(0, 3),
])
self.radius = max((ob.length for ob in shape))
super().__init__(position, shape, timestamp)
self._velocity = velocity

def _get_transform(self, dt):
# Set rotation in analogue to direction of velocity
position = Affine.translation(self._position + self._velocity * dt)
return position

def _update_time(self, timestamp):
dt = timestamp - self._timestamp
self._position = self._position + self._velocity * dt
self._timestamp = timestamp


class Bullet(PolygonComponent):

BULLET_SPEED = 120
BULLET_TTL = 0.8

def __init__(
self,
position: Vec2,
direction: Vec2,
timestamp: int):
r = self.radius = 0.15
self.ttl = self.BULLET_TTL
shape = Polygon([
Vec2(r, r), Vec2(-r, r),
Vec2(-r, -r), Vec2(r, -r),
])
super().__init__(position, shape, timestamp)
self._velocity = direction * self.BULLET_SPEED

def draw(self, timestamp):
assert self.timestamp + self.ttl > timestamp, \
"Why do we draw an expired bullet"
super().draw(timestamp)

def _get_transform(self, dt):
# Set rotation in analogue to direction of velocity
rotation = Affine.rotation(self._velocity.angle)
position = Affine.translation(self._position + self._velocity * dt)
return position * rotation

def _update_time(self, timestamp):
dt = timestamp - self._timestamp
self._position = self._position + self._velocity * dt
self._timestamp = timestamp


class SpaceShip(PolygonComponent):

ROTATION_SPEED = 12 # deg / s
BUMP_AMPLITUDE = 10
DECELERATION = 0.5

def __init__(
self,
position: Vec2,
velocity: Vec2,
angle: int,
timestamp: int,
):
shape = Polygon([
Vec2(4, 0), Vec2(-2, -2),
Vec2(-1, 0), Vec2(-2, 2),
])
super().__init__(position, shape, timestamp)
self._velocity = velocity
self._angle = angle

def _get_transform(self, dt):
# Set rotation in analogue to direction of velocity
rotation = Affine.rotation(self._angle)
position = Affine.translation(self._position_in(dt))
return position * rotation

def _position_in(self, dt):
# integral v/(1+b t) dt = (v log(b t+1))/b+constant
# where v - velocity, b - DECELERATION
return self._position + (
self._velocity * (
math.log(self.DECELERATION * dt + 1) / self.DECELERATION))

def _velocity_in(self, dt):
# v/(1+b t)
# where v - velocity, b - DECELERATION
return self._velocity / (1 + self.DECELERATION * dt)

def _update_time(self, timestamp):
dt = timestamp - self._timestamp
self._position = self._position_in(dt)
self._velocity = self._velocity_in(dt)
self._timestamp = timestamp

def bump(self, timestamp):
self._update_time(timestamp)
direction = Vec2.polar(self._angle)
self._velocity += direction * self.BUMP_AMPLITUDE
# FIXME: Reschedule world

def rotate(self, timestamp, direction):
if direction not in [1, -1]:
raise ValueError("`direction` should be 1 or -1")

self._update_time(timestamp)
self._angle += direction * self.ROTATION_SPEED
# FIXME: Reschedule world

def fire_bullet(self, timestamp):
dt = timestamp - self._timestamp
position = self._position_in(dt)
# Bullet is created at pin of the ship. Which is 4 units further than
# center
direction = Vec2.polar(self._angle)
position += direction * 4

bullet = Bullet(
position, direction, timestamp)

self._world._add_bullet(timestamp, bullet)
3 changes: 3 additions & 0 deletions gengine/examples/asteroids/config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[main]
x = 1

36 changes: 36 additions & 0 deletions gengine/examples/asteroids/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import argparse
import pathlib
import time
from .world import World


def get_parser():
parser = argparse.ArgumentParser(
description='Asteroids game')
# parser.add_argument(
# '--host', help='Host to connect to', default="localhost")
# parser.add_argument(
# '--port', help='Port to connect to', default=24000, type=int)
parser.add_argument(
'--config', help='Path to config.ini', default="config.ini",
type=pathlib.Path)
return parser


def main():
parser = get_parser()
args = parser.parse_args()

world = World()

try:
# We import here, so, that pyglet does not start before we parsed
# arguments. This is useful if we ask for command line help to not
# load GUI window.
import pyglet
from .window import GameWindow
GameWindow(world, 600, 600)

pyglet.app.run()
except KeyboardInterrupt:
print("Received SIG_INT. Quiting...")
139 changes: 139 additions & 0 deletions gengine/examples/asteroids/window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import math
import pyglet
from pyglet.window import key


from pyglet.gl import (
glBegin,
glVertex2f,
glEnd,
glDisable,
glViewport,
glMatrixMode,
glLoadIdentity,
glOrtho,
GL_TRIANGLE_FAN,
GL_DEPTH_TEST,
GL_PROJECTION,
GL_MODELVIEW,
GL_LINES,
glScalef
)

COLLISION_RADIUS = 3


class GameWindow(pyglet.window.Window):

SPACE_TTL = 0.08
LEFT_TTL = 1 / 30
RIGHT_TTL = 1 / 30
UP_TTL = 1 / 10

def __init__(self, world, *args, **kw):
super().__init__(*args, **kw)
self._world = world
self.fps_display = pyglet.clock.ClockDisplay()
self.keys = key.KeyStateHandler()
self.push_handlers(self.keys)
pyglet.clock.schedule_interval(self._tick, 1 / 60)
# Key trottle times
self._last_calls = {}

def _tick(self, dt):
self._world.advance_by(dt)
self.check_keys()

def check_keys(self):
timestamp = self._world.time()
if self.keys[key.SPACE]:
last_called = self._last_calls.get(key.SPACE, 0)
if timestamp - last_called >= self.SPACE_TTL:
self._last_calls[key.SPACE] = timestamp
self.trigger_space(timestamp)
if self.keys[key.LEFT]:
last_called = self._last_calls.get(key.LEFT, 0)
if timestamp - last_called >= self.LEFT_TTL:
self._last_calls[key.LEFT] = timestamp
self.trigger_left(timestamp)
if self.keys[key.RIGHT]:
last_called = self._last_calls.get(key.RIGHT, 0)
if timestamp - last_called >= self.RIGHT_TTL:
self._last_calls[key.RIGHT] = timestamp
self.trigger_right(timestamp)
if self.keys[key.UP]:
last_called = self._last_calls.get(key.UP, 0)
if timestamp - last_called >= self.UP_TTL:
self._last_calls[key.UP] = timestamp
self.trigger_up(timestamp)

def trigger_space(self, timestamp):
self._world.player.fire_bullet(timestamp)

def trigger_left(self, timestamp):
self._world.player.rotate(timestamp, 1)

def trigger_right(self, timestamp):
self._world.player.rotate(timestamp, -1)

def trigger_up(self, timestamp):
self._world.player.bump(timestamp)

def on_draw(self):
self.clear()
self.set_2d()
self._draw_static()
self._draw_objects()
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
self.fps_display.draw()

def _draw_static(self):
# No static for now =)
pass

def _draw_objects(self):
timestamp = self._world.time()
for component in self._world._objects:
component.draw(timestamp)

def set_2d(self):
screen_width, screen_height = self.get_size()
width, height = self._world.width, self._world.height

glDisable(GL_DEPTH_TEST)
glViewport(0, 0, screen_width, screen_height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(
-screen_width/2, screen_width/2,
-screen_height/2, screen_height/2,
-1, 1)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
# We add 10% to bounds so Asteroids port between bounds smoothly
glScalef(screen_width / width * 1.1,
screen_height / height * 1.1, 1.0)


def circle(x, y, radius):
"""
We want a pixel perfect circle. To get one,
we have to approximate it densely with triangles.
Each triangle thinner than a pixel is enough
to do it. Sin and cosine are calculated once
and then used repeatedly to rotate the vector.
I dropped 10 iterations intentionally for fun.
"""
iterations = int(2*radius*math.pi)
s = math.sin(2*math.pi / iterations)
c = math.cos(2*math.pi / iterations)

dx, dy = radius, 0

glBegin(GL_TRIANGLE_FAN)
glVertex2f(x, y)
for i in range(iterations+1):
glVertex2f(x+dx, y+dy)
dx, dy = (dx*c - dy*s), (dy*c + dx*s)
glEnd()
Loading

0 comments on commit 3e88ba1

Please sign in to comment.