Add world map
This commit is contained in:
parent
7f5129b770
commit
0a3e1681c8
|
@ -0,0 +1 @@
|
|||
include LICENSE
|
|
@ -0,0 +1 @@
|
|||
"""Axuy is a minimalist first-person shooter."""
|
|
@ -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()
|
Binary file not shown.
|
@ -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)
|
|
@ -0,0 +1,9 @@
|
|||
#version 330
|
||||
|
||||
uniform vec3 color;
|
||||
in float alpha;
|
||||
out vec4 f_color;
|
||||
|
||||
void main() {
|
||||
f_color = vec4(color, alpha);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
|
@ -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']})
|
|
@ -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')
|
Loading…
Reference in New Issue