Add world map

This commit is contained in:
Nguyễn Gia Phong 2019-02-06 23:08:24 +07:00
parent 7f5129b770
commit 0a3e1681c8
11 changed files with 367 additions and 0 deletions

1
MANIFEST.in Normal file
View File

@ -0,0 +1 @@
include LICENSE

1
axuy/__init__.py Normal file
View File

@ -0,0 +1 @@
"""Axuy is a minimalist first-person shooter."""

94
axuy/main.py Normal file
View File

@ -0,0 +1,94 @@
# main.py - start game and main loop
# Copyright (C) 2019 Nguyễn Gia Phong
#
# This file is part of Axuy
#
# Axuy is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Axuy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
from random import shuffle
import glfw
import moderngl
from .misc import hex2f4
from .view import View
FOV_INIT = 90
FOV_MIN = 30
FOV_MAX = 120
def main():
"""Create window, OpenGL context and start main loop."""
if not glfw.init():
print('Failed to initialize glfw!')
return
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3)
glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE)
glfw.window_hint(glfw.OPENGL_FORWARD_COMPAT, True)
window = glfw.create_window(640, 480, 'Axuy', None, None)
if not window:
print('Failed to create glfw window!')
glfw.terminate()
return
glfw.make_context_current(window)
glfw.swap_interval(1)
glfw.set_input_mode(window, glfw.CURSOR, glfw.CURSOR_DISABLED)
glfw.set_input_mode(window, glfw.STICKY_KEYS, True)
context = moderngl.create_context()
context.enable(moderngl.BLEND)
fov = FOV_INIT
def zoom(window, xoffset, yoffset):
"""Adjust FOV according to vertical scroll."""
nonlocal fov
fov += yoffset
if fov < FOV_MIN: fov = FOV_MIN
if fov > FOV_MAX: fov = FOV_MAX
glfw.set_scroll_callback(window, zoom)
mapid = list(range(48))
shuffle(mapid)
view = View(mapid, context)
glfw.set_cursor_pos_callback(window, view.look)
last_time = glfw.get_time()
while not glfw.window_should_close(window):
next_time = glfw.get_time()
view.fps = 1 / (next_time-last_time)
last_time = next_time
if glfw.get_key(window, glfw.KEY_UP) == glfw.PRESS:
view.move(view.forward)
if glfw.get_key(window, glfw.KEY_DOWN) == glfw.PRESS:
view.move(-view.forward)
if glfw.get_key(window, glfw.KEY_LEFT) == glfw.PRESS:
view.move(-view.right)
if glfw.get_key(window, glfw.KEY_RIGHT) == glfw.PRESS:
view.move(view.right)
width, height = glfw.get_window_size(window)
context.viewport = 0, 0, width, height
context.clear(*hex2f4('2e3436'))
view.render(width, height, fov)
glfw.swap_buffers(window)
glfw.set_cursor_pos(window, width/2, height/2)
glfw.poll_events()
glfw.terminate()

BIN
axuy/map.npy Normal file

Binary file not shown.

30
axuy/misc.py Normal file
View File

@ -0,0 +1,30 @@
# misc.py - miscellaneous functions
# Copyright (C) 2019 Nguyễn Gia Phong
#
# This file is part of Axuy
#
# Axuy is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Axuy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
import numpy
import pkg_resources
def hex2f4(hex_color):
"""Return numpy float32 array of RGB colors from given hex_color."""
return numpy.float32([i / 255 for i in bytes.fromhex(hex_color)])
def resource_filename(resource_name):
"""Return a true filesystem path for the specified resource."""
return pkg_resources.resource_filename('axuy', resource_name)

9
axuy/space.frag Normal file
View File

@ -0,0 +1,9 @@
#version 330
uniform vec3 color;
in float alpha;
out vec4 f_color;
void main() {
f_color = vec4(color, alpha);
}

11
axuy/space.vert Normal file
View File

@ -0,0 +1,11 @@
#version 330
uniform mat4 mvp;
uniform vec3 eye;
in vec3 in_vert;
out float alpha;
void main() {
gl_Position = mvp * vec4(in_vert, 1.0);
alpha = 1 - distance(eye, in_vert) / 4;
}

153
axuy/view.py Normal file
View File

@ -0,0 +1,153 @@
# view.py - maintain view on game world
# Copyright (C) 2019 Nguyễn Gia Phong
#
# This file is part of Axuy
#
# Axuy is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Axuy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Axuy. If not, see <https://www.gnu.org/licenses/>.
from itertools import chain, combinations_with_replacement, permutations
from math import cos, sin, pi
from random import random
import glfw
import moderngl
import numpy as np
from pyrr import Matrix44
from .misc import hex2f4, resource_filename
# map.npy is generated by ../tools/mapgen
SPACE = np.load(resource_filename('map.npy'))
OXY = np.float32([[0, 0, 0], [1, 0, 0], [1, 1, 0],
[1, 1, 0], [0, 1, 0], [0, 0, 0]])
OYZ = np.float32([[0, 0, 0], [0, 1, 0], [0, 1, 1],
[0, 1, 1], [0, 0, 1], [0, 0, 0]])
OZX = np.float32([[0, 0, 0], [1, 0, 0], [1, 0, 1],
[1, 0, 1], [0, 0, 1], [0, 0, 0]])
NEIGHBORS = set(chain.from_iterable(map(
permutations, combinations_with_replacement(range(-1, 2), 3))))
SPEED = 2
MOUSE_SPEED = 1
with open(resource_filename('space.vert')) as f: VERTEX_SHADER = f.read()
with open(resource_filename('space.frag')) as f: FRAGMENT_SHADER = f.read()
class View:
"""World map and camera placement.
Parameters
----------
mapid : iterable of ints
order of nodes to sort map.npy.
context : moderngl.Context
OpenGL context from which ModernGL objects are created.
Attributes
----------
space : np.ndarray of bools
3D array of occupied space.
oxy, oyz, ozx : set of tuples of ints
Coordinates of cube faces to be rendered.
prog : moderngl.Program
Processed executable code in GLSL.
vao : moderngl.VertexArray
Vertex data of the map.
pos : np.ndarray of np.float32
Camera position.
hangle, vangle : floats
Viewing angle.
forward, up, right : np.ndarray of np.float32
Directions.
fps : float
Currently rendered frames per second.
"""
def __init__(self, mapid, context):
space = np.stack([SPACE[i] for i in mapid]).reshape(4, 4, 3, 3, 3, 3)
self.space = np.zeros([12, 12, 9])
self.oxy, self.oyz, self.ozx = set(), set(), set()
for (x, y, z, i, j, k), occupied in np.ndenumerate(space):
if occupied: self.set_cube(x*3 + i, y*3 + j, z*3 + k)
self.prog = context.program(vertex_shader=VERTEX_SHADER,
fragment_shader=FRAGMENT_SHADER)
self.prog['color'].write(hex2f4('eeeeec').tobytes())
vertices = []
for i in self.oxy: vertices.extend(i+j for j in OXY)
for i in self.oyz: vertices.extend(i+j for j in OYZ)
for i in self.ozx: vertices.extend(i+j for j in OZX)
vbo = context.buffer(np.stack(vertices).astype('f4').tobytes())
self.vao = context.simple_vertex_array(self.prog, vbo, 'in_vert')
x, y, z = random()*12, random()*12, random()*9
while self.space[int(x)][int(y)][int(z)]:
x, y, z = random()*12, random()*12, random()*9
self.pos = np.float32([x, y, z])
self.hangle = random() * pi * 2
self.vangle = 0
self.set_directions()
self.fps = 60.0
def set_cube(self, x, y, z):
"""Mark occupied space and faces for rendering."""
i, j, k = (x+1) % 12, (y+1) % 12, (z+1) % 9
for tx, ty, tz in NEIGHBORS:
xt, yt, zt = x + tx*12, y + ty*12, z + tz*9
it, jt, kt = i + tx*12, j + ty*12, k + tz*9
self.oxy.update(((xt, yt, zt), (xt, yt, kt)))
self.oyz.update(((xt, yt, zt), (it, yt, zt)))
self.ozx.update(((xt, yt, zt), (xt, jt, zt)))
self.space[x][y][z] = 1
def set_directions(self):
"""Set forward, up and right directions based on view angle."""
self.forward = np.float32([cos(self.vangle) * sin(self.hangle),
sin(self.vangle),
cos(self.vangle) * cos(self.hangle)])
self.right = np.float32([-cos(self.hangle), 0, sin(self.hangle)])
self.up = np.cross(self.right, self.forward)
def move(self, direction):
"""Move camera in the given direction."""
dr = direction / self.fps * SPEED
i, j, k = self.pos + dr
if not self.space[int(i%12)][int(j%12)][int(k%9)]: self.pos += dr
x, y, z = self.pos
self.pos = x%12, y%12, z%9
def look(self, window, xpos, ypos):
"""Look according to cursor position.
Present as a callback for GLFW CursorPos event.
"""
center = np.float32(glfw.get_window_size(window)) / 2
dh, dv = center - [xpos, ypos]
self.vangle += MOUSE_SPEED / self.fps * dv
if cos(self.vangle) > 0:
self.hangle += MOUSE_SPEED / self.fps * dh
else:
self.hangle -= MOUSE_SPEED / self.fps * dh
self.set_directions()
def render(self, width, height, fov):
"""Render the map."""
proj = Matrix44.perspective_projection(fov, width/height, 0.0001, 4)
look = Matrix44.look_at(self.pos, self.pos + self.forward, self.up)
self.prog['mvp'].write((proj*look).astype(np.float32).tobytes())
self.prog['eye'].write(np.float32(self.pos).tobytes())
self.vao.render(moderngl.TRIANGLES)

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[bdist_wheel]
universal=1

32
setup.py Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
from setuptools import setup
with open('README.md') as f:
long_description = f.read()
setup(
name='axuy',
version='0.0.1',
description='Minimalist first-person shooter',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://github.com/McSinyx/axuy',
author='Nguyễn Gia Phong',
author_email='vn.mcsinyx@gmail.com',
license='AGPLv3+',
classifiers=[
'Development Status :: 1 - Planning',
'Environment :: MacOS X',
'Environment :: Win32 (MS Windows)',
'Environment :: X11 Applications',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
'Natural Language :: English',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Games/Entertainment :: First Person Shooters'],
keywords='fps opengl glfw',
packages=['axuy'],
install_requires=['numpy', 'pyrr', 'moderngl', 'glfw'],
package_data={'axuy': ['map.npy', 'space.vert', 'space.frag']},
entry_points={'console_scripts': ['axuy = axuy.main:main']})

34
tools/mapgen Executable file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
from itertools import chain
from sys import argv
from numpy import fliplr, flipud, frombuffer, fromiter, save, stack
BASES = ('000111000000111000000000000', '000111000000111000000111000',
'000111000000111111000000000', '000111000111111111000000000',
'000111000111111111000111000', '000111000111111111010010010')
def chainmap(sequence, *functions):
"""Composingly chain and map sequence according to functions."""
if not functions: return sequence
first, *rest = functions
return chainmap(chain.from_iterable(map(first, sequence)), *rest)
def permute(bases):
"""Return rotations (possibly with duplication) of base nodes."""
return chainmap(bases,
lambda a: (a, fliplr(a)),
lambda a: (a, flipud(a)),
lambda a: (a, a.swapaxes(1, 2)),
lambda a: (a, a.swapaxes(0, 1), a.swapaxes(0, 2)))
p = permute(fromiter(map(int, base), bool).reshape(3, 3, 3) for base in BASES)
uniques = [frombuffer(s, bool) for s in set(node.tobytes() for node in p)]
try:
with open(argv[1], 'w+b') as f: save(f, stack(uniques))
except (IndexError, IOError):
print('Usage: mapgen output-file')