1
0
Fork 0
mirror of https://github.com/quantum5/punyverse.git synced 2025-04-24 21:21:59 -04:00
punyverse/punyverse/game.py
2018-08-22 17:42:34 -04:00

500 lines
18 KiB
Python

#!/usr/bin/python
from operator import attrgetter
from math import hypot, sqrt, atan2, degrees
from time import clock
import time
import random
import os
import six
from punyverse.camera import Camera
from punyverse.world import World
from punyverse.glgeom import *
from punyverse.entity import Asteroid
from punyverse import texture
try:
from punyverse._model import model_list, load_model
except ImportError:
from punyverse.model import model_list, load_model
try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
from pyglet.gl import *
from pyglet.window import key, mouse
import pyglet
INITIAL_SPEED = 0 # The initial speed of the player
MOUSE_SENSITIVITY = 0.3 # Mouse sensitivity, 0..1, none...hyperspeed
MAX_DELTA = 5
SEED = int(time.time())
def entity_distance(x0, y0, z0):
def distance(entity):
x1, y1, z1 = entity.location
return hypot(hypot(x1 - x0, y1 - y0), z1 - z0)
return distance
class Applet(pyglet.window.Window):
def __init__(self, *args, **kwargs):
self.world_options = kwargs.pop('world_options', {})
super(Applet, self).__init__(*args, **kwargs)
texture.init()
if hasattr(self.config, '_attribute_names'):
info = [' %-22s %s' % (key + ':', getattr(self.config, key))
for key in self.config._attribute_names]
info = ['%-30s %-30s' % group for group in
zip_longest(info[::2], info[1::2], fillvalue='')]
info = 'OpenGL configuration:\n' + '\n'.join(info)
else:
info = 'Unknown OpenGL configuration'
self.loaded = False
self.__load_started = False
self._loading_phase = pyglet.text.Label(
font_name='Consolas', font_size=20, x=10, y=self.height - 80,
color=(255, 255, 255, 255), width=self.width - 20, align='center',
multiline=True, text='Punyverse is starting...'
)
self._loading_label = pyglet.text.Label(
font_name='Consolas', font_size=16, x=10, y=self.height - 150,
color=(255, 255, 255, 255), width=self.width - 20, align='center',
multiline=True
)
self._info_label = pyglet.text.Label(
font_name='Consolas', font_size=13, x=10, y=self.height - 250,
color=(255, 255, 255, 255), width=self.width - 20,
multiline=True, text=info
)
pyglet.clock.schedule_once(self.load, 0)
def load(self, *args, **kwargs):
if self.loaded or self.__load_started:
return
self.__load_started = True
def callback(phase, message, progress):
self.draw_loading(phase, message, progress)
self.flip()
self.dispatch_events()
start = clock()
self.fps = 0
self.world = World('world.json', callback, self.world_options)
phase = 'Initializing game...'
print(phase)
callback(phase, '', 0)
self.speed = INITIAL_SPEED
self.keys = set()
self.info = True
self.debug = False
self.orbit = True
self.running = True
self.moving = True
self.info_precise = False
self.atmosphere = True
self.cloud = not texture.badcard
self.tick = self.world.tick_length
self.ticks = [1, 2, 5, 10, 20, 40, 60, # Second range
120, 300, 600, 1200, 1800, 2700, 3600, # Minute range
7200, 14400, 21600, 43200, 86400, # Hour range
172800, 432000, 604800, # 2, 5, 7 days
1209600, 2592000, # 2 week, 1 month
5270400, 7884000, 15768000, 31536000, # 2, 3, 6, 12 months
63072000, 157680000, 315360000, # 2, 5, 10 years
630720000, 1576800000, 3153600000, # 20, 50, 100 years
]
self.__time_per_second_cache = None
self.__time_per_second_value = None
self.__time_accumulate = 0
def speed_incrementer(object, increment):
def incrementer():
object.speed += increment
return incrementer
def attribute_toggler(object, attribute):
getter = attrgetter(attribute)
def toggler():
setattr(object, attribute, not getter(object))
return toggler
def increment_tick():
index = self.ticks.index(self.tick) + 1
if index < len(self.ticks):
self.tick = self.ticks[index]
def decrement_tick():
index = self.ticks.index(self.tick) - 1
if index >= 0:
self.tick = self.ticks[index]
self.key_handler = {
key.ESCAPE: pyglet.app.exit,
key.NUM_ADD: speed_incrementer(self, 1),
key.NUM_SUBTRACT: speed_incrementer(self, -1),
key.NUM_MULTIPLY: speed_incrementer(self, 10),
key.NUM_DIVIDE: speed_incrementer(self, -10),
key.PAGEUP: speed_incrementer(self, 100),
key.PAGEDOWN: speed_incrementer(self, -100),
key.HOME: speed_incrementer(self, 1000),
key.END: speed_incrementer(self, -1000),
key.R: lambda: setattr(self.cam, 'roll', 0),
key.I: attribute_toggler(self, 'info'),
key.D: attribute_toggler(self, 'debug'),
key.O: attribute_toggler(self, 'orbit'),
key.P: attribute_toggler(self, 'info_precise'),
key.C: attribute_toggler(self, 'cloud'),
key.X: attribute_toggler(self, 'atmosphere'),
key.ENTER: attribute_toggler(self, 'running'),
key.INSERT: increment_tick,
key.DELETE: decrement_tick,
key.SPACE: self.launch_meteor,
key.E: lambda: self.set_exclusive_mouse(False),
key.F: lambda: self.set_fullscreen(not self.fullscreen),
}
self.mouse_press_handler = {
mouse.LEFT: self.launch_meteor,
mouse.RIGHT: attribute_toggler(self, 'moving'),
}
self.label = pyglet.text.Label('', font_name='Consolas', font_size=12, x=10, y=self.height - 20,
color=(255,) * 4, multiline=True, width=600)
self.cam = Camera()
self.exclusive = False
glClearColor(0, 0, 0, 1)
glClearDepth(1.0)
glAlphaFunc(GL_GEQUAL, 0.2)
glDepthFunc(GL_LEQUAL)
glEnable(GL_DEPTH_TEST)
glShadeModel(GL_SMOOTH)
glMatrixMode(GL_MODELVIEW)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)
glEnable(GL_LIGHT1)
glEnable(GL_POLYGON_OFFSET_FILL)
fv4 = GLfloat * 4
glLightfv(GL_LIGHT0, GL_POSITION, fv4(.5, .5, 1, 0))
glLightfv(GL_LIGHT0, GL_SPECULAR, fv4(.5, .5, 1, 1))
glLightfv(GL_LIGHT0, GL_DIFFUSE, fv4(1, 1, 1, 1))
glLightfv(GL_LIGHT1, GL_POSITION, fv4(1, 0, .5, 0))
glLightfv(GL_LIGHT1, GL_DIFFUSE, fv4(.5, .5, .5, 1))
glLightfv(GL_LIGHT1, GL_SPECULAR, fv4(1, 1, 1, 1))
phase = 'Loading asteroids...'
print(phase)
def load_asteroids(files):
for id, file in enumerate(files):
callback(phase, 'Loading %s...' % file, float(id) / len(files))
yield model_list(load_model(file), 5, 5, 5, (0, 0, 0))
self.asteroid_ids = list(load_asteroids([r'asteroids/01.obj', r'asteroids/02.obj', r'asteroids/03.obj']))
c = self.cam
c.x, c.y, c.z = self.world.start
c.pitch, c.yaw, c.roll = self.world.direction
phase = 'Updating entities...'
print(phase)
callback(phase, '', 0)
for entity in self.world.tracker:
entity.update()
print('Loaded in %s seconds.' % (clock() - start))
self.loaded = True
pyglet.clock.schedule(self.update)
self.on_resize(self.width, self.height) # On resize handler does nothing unless it's loaded
def screenshot(self):
image = pyglet.image.get_buffer_manager().get_color_buffer()
if hasattr(self, '_hwnd') and not self.modifiers & key.MOD_CTRL:
from ctypes import windll, cdll
from PIL import Image
import tempfile
CF_BITMAP = 2
image = Image.frombytes(image.format, (image.width, image.height), image.get_image_data().data)
image = image.convert('RGB').transpose(Image.FLIP_TOP_BOTTOM)
fd, filename = tempfile.mkstemp('.bmp')
try:
with os.fdopen(fd, 'wb') as file:
image.save(file, 'BMP')
if isinstance(filename, six.binary_type):
filename = filename.decode('mbcs' if os.name == 'nt' else 'utf8')
image = windll.user32.LoadImageW(None, filename, 0, 0, 0, 0x10)
windll.user32.OpenClipboard(self._hwnd)
windll.user32.EmptyClipboard()
windll.user32.SetClipboardData(CF_BITMAP, image)
windll.user32.CloseClipboard()
finally:
os.remove(filename)
else:
image.save(os.path.expanduser('~/punyverse.png'))
def set_exclusive_mouse(self, exclusive):
super(Applet, self).set_exclusive_mouse(exclusive)
self.exclusive = exclusive
def launch_meteor(self):
c = self.cam
dx, dy, dz = c.direction()
speed = abs(self.speed) * 1.1 + 5
dx *= speed
dy *= speed
dz *= speed
self.world.tracker.append(Asteroid(random.choice(self.asteroid_ids), (c.x, c.y - 3, c.z + 5),
direction=(dx, dy, dz)))
def on_mouse_press(self, x, y, button, modifiers):
self.modifiers = modifiers
if not self.loaded:
return
if not self.exclusive:
self.set_exclusive_mouse(True)
else:
if button in self.mouse_press_handler:
self.mouse_press_handler[button]()
def on_mouse_motion(self, x, y, dx, dy):
if not self.loaded:
return
if self.exclusive: # Only handle camera movement if mouse is grabbed
self.cam.mouse_move(dx * MOUSE_SENSITIVITY, dy * MOUSE_SENSITIVITY)
def on_key_press(self, symbol, modifiers):
self.modifiers = modifiers
if symbol == key.Q:
self.screenshot()
if not self.loaded:
return
if self.exclusive: # Only handle keyboard input if mouse is grabbed
if symbol in self.key_handler:
self.key_handler[symbol]()
else:
self.keys.add(symbol)
def on_key_release(self, symbol, modifiers):
if not self.loaded:
return
if symbol in self.keys:
self.keys.remove(symbol)
def on_resize(self, width, height):
if not self.loaded:
return super(Applet, self).on_resize(width, height)
height = max(height, 1) # Prevent / by 0
self.label.y = height - 20
glViewport(0, 0, width, height)
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
# A field of view of 45
gluPerspective(45.0, width / float(height), 1, 50000000.0)
glMatrixMode(GL_MODELVIEW)
def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
if not self.loaded:
return
self.speed += scroll_y * 50 + scroll_x * 500
def get_time_per_second(self):
if self.__time_per_second_cache == self.tick:
return self.__time_per_second_value
time = self.tick + .0
unit = 'seconds'
for size, name in ((60, 'minutes'), (60, 'hours'), (24, 'days'), (365, 'years')):
if time < size:
break
time /= size
unit = name
result = '%s %s' % (round(time, 1), unit)
self.__time_per_second_cache = self.tick
self.__time_per_second_value = result
return result
def update(self, dt):
c = self.cam
if self.exclusive:
if key.A in self.keys:
c.roll += 4 * dt * 10
if key.S in self.keys:
c.roll -= 4 * dt * 10
if self.moving:
c.move(self.speed * 10 * dt)
if self.running:
delta = self.tick * dt
update = int(delta + self.__time_accumulate + 0.5)
if update:
self.__time_accumulate = 0
self.world.tick += update
for entity in self.world.tracker:
entity.update()
collision = entity.collides(c.x, c.y, c.z)
if collision:
self.speed *= -1
c.move(self.speed * 12 * dt)
else:
self.__time_accumulate += delta
def draw_loading(self, phase=None, message=None, progress=None):
glClear(GL_COLOR_BUFFER_BIT)
glLoadIdentity()
if phase is not None:
self._loading_phase.text = phase
if message is not None:
self._loading_label.text = message
self._loading_phase.draw()
self._loading_label.draw()
if progress is not None:
progress_bar(10, self.height - 170, self.width - 20, 50, progress)
self._info_label.draw()
def on_draw(self, glMatrixBuffer=GLfloat * 16):
if not self.loaded:
return self.draw_loading()
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glLoadIdentity()
c = self.cam
x, y, z = c.x, c.y, c.z
glRotatef(c.pitch, 1, 0, 0)
glRotatef(c.yaw, 0, 1, 0)
glRotatef(c.roll, 0, 0, 1)
glTranslatef(-x, -y, -z)
glEnable(GL_LIGHTING)
world = self.world
get_distance = entity_distance(x, y, z)
if x != world.x or y != world.y or z != world.z:
world.tracker.sort(key=get_distance, reverse=True)
world.tracker.sort(key=attrgetter('background'), reverse=True)
world.x, world.y, world.z = x, y, z
for entity in world.tracker:
x, y, z = entity.location
pitch, yaw, roll = entity.rotation
with glMatrix(), glRestore(GL_CURRENT_BIT):
if entity.background:
glTranslatef(c.x, c.y, c.z)
else:
glTranslatef(x, y, z)
glRotatef(pitch, 1, 0, 0)
glRotatef(yaw, 0, 1, 0)
glRotatef(roll, 0, 0, 1)
glCallList(entity.id)
if self.debug:
with glMatrix(), glRestore(GL_ENABLE_BIT | GL_POLYGON_BIT | GL_LINE_BIT):
glLineWidth(0.25)
glPolygonOffset(1, 1)
glDisable(GL_LIGHTING)
glDisable(GL_TEXTURE_2D)
glColor3f(0, 1, 0)
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
glCallList(entity.id)
has_corona = hasattr(entity, 'corona') and entity.corona
has_atmosphere = hasattr(entity, 'atmosphere') and entity.atmosphere
if self.atmosphere and (has_corona or has_atmosphere):
with glMatrix(), glRestore(GL_ENABLE_BIT):
x0, y0, z0 = entity.location
glTranslatef(x0, y0, z0)
matrix = glMatrixBuffer()
glGetFloatv(GL_MODELVIEW_MATRIX, matrix)
matrix[0: 3] = [1, 0, 0]
matrix[4: 7] = [0, 1, 0]
matrix[8:11] = [0, 0, 1]
glLoadMatrixf(matrix)
glEnable(GL_BLEND)
if has_atmosphere:
glCallList(entity.atmosphere)
if has_corona:
x, y, z = c.direction()
glTranslatef(-x, -y, -z)
glCallList(entity.corona)
if self.cloud and hasattr(entity, 'cloudmap') and entity.cloudmap:
with glMatrix(), glRestore(GL_ENABLE_BIT):
glEnable(GL_BLEND)
glEnable(GL_ALPHA_TEST)
glTranslatef(*entity.location)
pitch, yaw, roll = entity.rotation
glRotatef(pitch, 1, 0, 0)
glRotatef(yaw, 0, 1, 0)
glRotatef(roll, 0, 0, 1)
glCallList(entity.cloudmap)
if self.orbit and hasattr(entity, 'get_orbit') and hasattr(entity, 'parent'):
parent = entity.parent
distance = get_distance(parent)
if distance < parent.orbit_show:
with glMatrix(), glRestore(GL_ENABLE_BIT | GL_LINE_BIT | GL_CURRENT_BIT):
glTranslatef(*entity.parent.location)
glDisable(GL_LIGHTING)
solid = distance < parent.orbit_opaque
glColor4f(1, 1, 1, 1 if solid else
(1 - (distance - parent.orbit_opaque) / parent.orbit_blend))
if not solid:
glEnable(GL_BLEND)
glLineWidth(1)
glCallList(entity.get_orbit())
glColor4f(1, 1, 1, 1)
glDisable(GL_TEXTURE_2D)
width, height = self.get_size()
if self.info:
ortho(width, height)
if self.info_precise:
info = ('%d FPS @ (x=%.2f, y=%.2f, z=%.2f) @ %s, %s/s\n'
'Direction(pitch=%.2f, yaw=%.2f, roll=%.2f)\nTick: %d' %
(pyglet.clock.get_fps(), c.x, c.y, c.z, self.speed, self.get_time_per_second(),
c.pitch, c.yaw, c.roll, self.world.tick))
else:
info = ('%d FPS @ (x=%.2f, y=%.2f, z=%.2f) @ %s, %s/s\n' %
(pyglet.clock.get_fps(), c.x, c.y, c.z, self.speed, self.get_time_per_second()))
self.label.text = info
self.label.draw()
with glRestore(GL_CURRENT_BIT | GL_LINE_BIT):
glLineWidth(2)
cx, cy = width / 2, height / 2
glColor4f(0, 1, 0, 1)
circle(10, 20, (cx, cy))
frustrum()