Introduce Shard object

Only works locally at the moment and not optimized.
This commit is contained in:
Nguyễn Gia Phong 2019-03-01 11:02:00 +07:00
parent 809a577f6d
commit 74ac31d239
4 changed files with 241 additions and 98 deletions

View File

@ -16,7 +16,8 @@
# 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 itertools import (chain, combinations_with_replacement,
permutations, product)
from random import choices, shuffle
import numpy
@ -73,8 +74,34 @@ def neighbors(x, y, z):
for i, j, k in NEIGHBORS: yield x + i*12, y + j*12, z + k*9
def sign(x):
def normalized(*vector):
"""Return normalized vector as a NumPy array of float32."""
v = numpy.float32(vector)
if not any(v): return v
return v / sum(v**2)
def sign(x) -> int:
"""Return the sign of number x."""
if x > 0: return 1
if x: return -1
return 0
def twelve(x) -> int:
"""Shorthand for int(x % 12)."""
return int(x % 12)
def nine(x) -> int:
"""Shorthand for int(x % 9)."""
return int(x % 9)
def placeable(space, x, y, z, r):
"""Return whether a sphere of radius r
can be placed at (x, y, z) in given space."""
return not any(space[i][j][k] for i, j, k in product(
{twelve(x-r), twelve(x), twelve(x+r)},
{twelve(y-r), twelve(y), twelve(y+r)},
{nine(z-r), nine(z), nine(z+r)}))

View File

@ -24,8 +24,7 @@ from socket import socket, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR
from threading import Thread
from .misc import mapgen, mapidgen
from .pico import Picobot
from .view import View
from .view import Pico, View
class Peer:
@ -47,8 +46,8 @@ class Peer:
self.peers.extend(data['peers'])
self.space = mapgen(mapid)
self.pico = Picobot(self.space, (0, 0, 0))
self.view = View(self.pico, args.width, args.height, self.space)
self.pico = Pico(self.space, (0, 0, 0))
self.view = View(self.pico, self.space, args.width, args.height)
address = args.host, args.port
data_server = Thread(target=self.serve,
@ -69,15 +68,14 @@ class Peer:
def serve(self, address, mapid):
"""Initiate peers."""
self.server = socket() # TCP server
self.server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.server.bind(address)
self.server.listen(7)
while self.view.is_running:
conn, addr = self.server.accept()
conn.send(dumps({'mapid': mapid, 'peers': self.peers}))
conn.close()
self.server.close()
with socket() as server: # TCP server
server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server.bind(address)
server.listen(7)
while self.view.is_running:
conn, addr = server.accept()
conn.send(dumps({'mapid': mapid, 'peers': self.peers}))
conn.close()
def push(self):
"""Send own state to peers."""
@ -93,13 +91,12 @@ class Peer:
try:
self.view.picos[addr].update(pos, rot)
except KeyError:
self.view.picos[addr] = Picobot(self.space, pos, rot)
self.view.picos[addr] = Pico(self.space, pos, rot)
self.peers.append(addr)
def __enter__(self): return self
def __exit__(self, exc_type, exc_value, traceback):
self.server.close()
self.sock.close()
self.view.close()

View File

@ -25,23 +25,28 @@ import glfw
import numpy as np
from pyrr import matrix33
BASE = np.float32([[1, 0, 0], [0, 1, 0], [0, 0, -1]])
RP = 1 / 4 # radius of a Picobot
from .misc import normalized, placeable
SPEED = 2
MOUSE_SPEED = 1/8
INVX = np.float32([[-1, 0, 0], [0, 1, 0], [0, 0, 1]])
INVY = np.float32([[1, 0, 0], [0, -1, 0], [0, 0, -1]])
INVZ = np.float32([[1, 0, 0], [0, 1, 0], [0, 0, -1]])
PICO_SPEED = 2 # in unit/s
SHARD_SPEED = PICO_SPEED * 243**0.25 # in unit/s
SHARD_LIFE = 11 / SHARD_SPEED # in seconds
class Picobot:
"""Game character.
"""Game character, which is represented as a regular tetrahedron
whose circumscribed sphere's radius is 1/4 unit.
Parameters
----------
space : np.ndarray of shape (12, 12, 9) of bools
3D array of occupied space.
pos : iterable of length 3 of floats
position : iterable of length 3 of floats, optional
Position.
rotation : np.ndarray of shape (3, 3) of np.float32
rotation : np.ndarray of shape (3, 3) of np.float32, optional
Rotational matrix.
Attributes
@ -50,12 +55,11 @@ class Picobot:
3D array of occupied space.
x, y, z : floats
Position.
rotation : np.ndarray of shape (3, 3) of np.float32
rot : np.ndarray of shape (3, 3) of np.float32
Rotational matrix.
fps : float
Currently rendered frames per second.
"""
def __init__(self, space, position=None, rotation=None):
self.space = space
if position is None:
@ -67,43 +71,13 @@ class Picobot:
self.x, self.y, self.z = position
if rotation is None:
self.rotation = BASE
self.rot = INVZ
self.rotate(random()*pi*2, random()*pi*2)
else:
self.rotation = rotation
self.rot = rotation
self.fps = 60.0
def update(self, position, rotation):
"""Update state."""
self.pos = position
self.rotation = rotation
def empty(self, x, y, z) -> bool:
"""Return whether a Picobot can be placed at (x, y, z)."""
if self.space[int((x-RP) % 12)][int(y % 12)][int(z % 9)]: return False
if self.space[int((x+RP) % 12)][int(y % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int((y-RP) % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int((y+RP) % 12)][int(z % 9)]: return False
if self.space[int(x % 12)][int(y % 12)][int((z-RP) % 9)]: return False
if self.space[int(x % 12)][int(y % 12)][int((z+RP) % 9)]: return False
return True
def rotate(self, yaw, pitch):
"""Rotate yaw radians around y-axis
and pitch radians around x-axis.
"""
self.rotation = (matrix33.create_from_x_rotation(pitch)
@ matrix33.create_from_y_rotation(yaw) @ self.rotation)
def move(self, right=0, upward=0, forward=0):
"""Try to move in the given direction."""
dr = [right, upward, forward] @ self.rotation / self.fps * SPEED
x, y, z = [self.x, self.y, self.z] + dr
if self.empty(x, self.y, self.z): self.x = x % 12
if self.empty(self.x, y, self.z): self.y = y % 12
if self.empty(self.x, self.y, z): self.z = z % 9
@property
def pos(self):
"""Position in a NumPy array."""
@ -116,12 +90,90 @@ class Picobot:
@property
def state(self):
"""Position and rotation."""
return self.pos, self.rotation
return self.pos, self.rot
def look(self, window, xpos, ypos):
"""Look according to cursor position.
def sync(self, position, rotation) -> None:
"""Synchronize state received from other peers."""
self.pos = position
self.rot = rotation
Present as a callback for GLFW CursorPos event.
def placeable(self, x, y, z) -> bool:
"""Return whether it can be placed at (x, y, z)."""
return placeable(self.space, x, y, z, 1/4)
def rotate(self, yaw, pitch):
"""Rotate yaw radians around y-axis
and pitch radians around x-axis.
"""
center = np.float32(glfw.get_window_size(window)) / 2
self.rotate(*((center - [xpos, ypos]) / self.fps * MOUSE_SPEED))
self.rot = (matrix33.create_from_x_rotation(pitch)
@ matrix33.create_from_y_rotation(yaw) @ self.rot)
def move(self, right=0, upward=0, forward=0):
"""Try to move in the given direction."""
direction = normalized(right, upward, forward) @ self.rot
x, y, z = self.pos + direction/self.fps*PICO_SPEED
if self.placeable(x, self.y, self.z): self.x = x % 12
if self.placeable(self.x, y, self.z): self.y = y % 12
if self.placeable(self.x, self.y, z): self.z = z % 9
class Shard:
"""Fragment broken or shot out of a Picobot, which is a regular
octahedron whose circumscribed sphere's radius is 1/12 unit.
Parameters
----------
space : np.ndarray of shape (12, 12, 9) of bools
3D array of occupied space.
position : iterable of length 3 of floats
Position.
rotation : np.ndarray of shape (3, 3) of np.float32
Rotational matrix.
fps : float, optional
Currently rendered frames per second.
Attributes
----------
space : np.ndarray of shape (12, 12, 9) of bools
3D array of occupied space.
x, y, z : floats
Position.
rot : np.ndarray of shape (3, 3) of np.float32
Rotational matrix.
power : float
Relative destructive power to the original.
fps : float
Currently rendered frames per second.
"""
def __init__(self, space, position, rotation, fps=60.0):
self.space = space
self.x, self.y, self.z = position
self.rot = rotation
self.fps = fps
@property
def pos(self):
"""Position in a NumPy array."""
return np.float32([self.x, self.y, self.z])
@pos.setter
def pos(self, position):
self.x, self.y, self.z = position
@property
def forward(self):
"""Direction in a NumPy array."""
return self.rot[-1]
def placeable(self, x, y, z) -> bool:
"""Return whether it can be placed at (x, y, z)."""
return placeable(self.space, x, y, z, r=1/12)
def update(self):
"""Update states."""
x, y, z = self.pos + self.forward/self.fps*SHARD_SPEED
if not self.placeable(x, self.y, self.z): self.rot = self.rot @ INVX
if not self.placeable(self.x, y, self.z): self.rot = self.rot @ INVY
if not self.placeable(self.x, self.y, z): self.rot = self.rot @ INVZ
self.pos += self.forward / self.fps * SHARD_SPEED
return self

View File

@ -20,17 +20,20 @@ __doc__ = 'Axuy module for map class'
from itertools import product
from math import sqrt
from multiprocessing import Pool
import glfw
import moderngl
import numpy as np
from pyrr import Matrix44
from .pico import INVZ, Picobot, Shard
from .misc import abspath, color, neighbors, sign
FOV_MIN = 30
FOV_MAX = 120
FOV_INIT = (FOV_MIN+FOV_MAX) / 2
MOUSE_SPEED = 1/8
OXY = np.float32([[0, 0, 0], [1, 0, 0], [1, 1, 0],
[1, 1, 0], [0, 1, 0], [0, 0, 0]])
@ -54,30 +57,59 @@ with open(abspath('shaders/pico.vert')) as f: PICO_VERTEX = f.read()
with open(abspath('shaders/pico.frag')) as f: PICO_FRAGMENT = f.read()
class Pico(Picobot):
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
self.rotate(*((center - [xpos, ypos]) / self.fps * MOUSE_SPEED))
class View:
"""World map and camera placement.
(Documentation below is not completed.)
Parameters
----------
mapid : iterable of length 48 of ints
order of nodes to sort map.npy.
context : moderngl.Context
OpenGL context from which ModernGL objects are created.
camera : Pico
Protagonist whose view is the camera.
space : np.ndarray of shape (12, 12, 9) of bools
3D array of occupied space.
width, height : ints
Window size.
Attributes
----------
space : np.ndarray of shape (12, 12, 9) of bools
3D array of occupied space.
camera : Pico
Protagonist whose view is the camera.
picos : dict of (str, Pico)
Enemies characters.
shards : list of Shards
Picobot fragments, which are capable of causing damage.
window : GLFW window
fov : int
horizontal field of view in degrees
context : moderngl.Context
OpenGL context from which ModernGL objects are created.
maprog : moderngl.Program
Processed executable code in GLSL.
Processed executable code in GLSL for map rendering.
mapva : moderngl.VertexArray
Vertex data of the map.
camera : Picobot
Protagonist whose view is the camera.
prog : moderngl.Program
Processed executable code in GLSL
for rendering picobots and their shards.
pva : moderngl.VertexArray
Vertex data of picobots.
sva : moderngl.VertexArray
Vertex data of shards.
last_time : float
timestamp in seconds of the previous frame
"""
def __init__(self, pico, width, height, space):
def __init__(self, camera, space, width, height):
# Create GLFW window
if not glfw.init(): raise RuntimeError('Failed to initialize GLFW!')
glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3)
@ -87,11 +119,13 @@ class View:
self.window = glfw.create_window(width, height, 'Axuy', None, None)
if not self.window:
glfw.terminate()
raise RuntimeError('Failed to create glfw window!')
raise RuntimeError('Failed to create GLFW window!')
self.camera = pico
self.picos = {'self': pico}
self.last_time = glfw.get_time() # to keep track of FPS
self.camera = camera
self.picos = {'self': camera}
self.shards = []
self.last_time = glfw.get_time()
self.pool = Pool()
# Window's rendering and event-handling configuration
glfw.make_context_current(self.window)
@ -101,6 +135,7 @@ class View:
glfw.set_cursor_pos_callback(self.window, self.camera.look)
self.fov = FOV_INIT
glfw.set_scroll_callback(self.window, self.zoom)
glfw.set_mouse_button_callback(self.window, self.shoot)
# Create OpenGL context
self.context = context = moderngl.create_context()
@ -128,8 +163,9 @@ class View:
pvb = [(context.buffer(TETRAVERTICES.tobytes()), '3f', 'in_vert')]
pib = context.buffer(TETRAINDECIES.tobytes())
self.pva = context.vertex_array(self.prog, pvb, pib)
self.should_close = None
svb = [(context.buffer(OCTOVERTICES.tobytes()), '3f', 'in_vert')]
sib = context.buffer(OCTOINDECIES.tobytes())
self.sva = context.vertex_array(self.prog, svb, sib)
def zoom(self, window, xoffset, yoffset):
"""Adjust FOV according to vertical scroll."""
@ -137,6 +173,16 @@ class View:
if self.fov < FOV_MIN: self.fov = FOV_MIN
if self.fov > FOV_MAX: self.fov = FOV_MAX
def shoot(self, window, button, action, mods):
"""Shoot on click.
Present as a callback for GLFW MouseButton event.
"""
protagonist = self.camera
if button == glfw.MOUSE_BUTTON_LEFT and action == glfw.PRESS:
self.shards.append(Shard(self.space, protagonist.pos,
protagonist.rot, protagonist.fps))
@property
def pos(self):
"""Camera position in a NumPy array."""
@ -145,32 +191,47 @@ class View:
@property
def right(self):
"""Camera right direction."""
return self.camera.rotation[0]
return self.camera.rot[0]
@property
def upward(self):
"""Camera upward direction."""
return self.camera.rotation[1]
return self.camera.rot[1]
@property
def forward(self):
"""Camera forward direction."""
return self.camera.rotation[2]
return self.camera.rot[2]
@property
def is_running(self):
def is_running(self) -> bool:
"""GLFW window status."""
return not glfw.window_should_close(self.window)
def is_pressed(self, *keys):
@property
def visibility(self) -> np.float32:
"""Camera visibility."""
return np.float32(sqrt(1800 / self.fov))
def update_fps(self, fps):
"""Update camera's and shards' FPS
for their movement calculations.
"""
self.camera.fps = fps
for shard in self.shards: shard.fps = fps
print(len(self.shards), fps)
def is_pressed(self, *keys) -> bool:
"""Return whether given keys are pressed."""
return any(glfw.get_key(self.window, k) == glfw.PRESS for k in keys)
def render(self, obj, va):
"""Render the obj and its images in bounded 3D space."""
rotation = Matrix44.from_matrix33(obj.rotation)
vsqr = self.visibility ** 2
rotation = Matrix44.from_matrix33(obj.rot)
i, j, k = map(sign, self.pos - obj.pos)
for position in product(*zip(obj.pos, obj.pos + [i*12, j*12, k*9])):
if sum((self.pos-position) ** 2) > vsqr: continue
model = rotation @ Matrix44.from_translation(position)
self.prog['model'].write(model.astype(np.float32).tobytes())
self.prog['color'].write(color('Background').tobytes())
@ -179,19 +240,20 @@ class View:
va.render(moderngl.TRIANGLES)
def render_pico(self, pico):
"""Render the pico and its images in bounded 3D space."""
rotation = Matrix44.from_matrix33(pico.rotation)
i, j, k = map(sign, self.pos - pico.pos)
for position in product(*zip(pico.pos, pico.pos + [i*12, j*12, k*9])):
model = rotation @ Matrix44.from_translation(position)
self.prog['model'].write(model.astype(np.float32).tobytes())
self.prog['color'].write(color('Background').tobytes())
self.pva.render(moderngl.LINES)
self.prog['color'].write(color('Plum').tobytes())
self.pva.render(moderngl.TRIANGLES)
"""Render pico and its images in bounded 3D space."""
self.render(pico, self.pva)
def render_shard(self, shard):
"""Render shard and its images in bounded 3D space."""
self.render(shard, self.sva)
def update(self):
"""Handle input, update GLSL programs and render the map."""
# Update instantaneous FPS
next_time = glfw.get_time()
self.update_fps(1 / (next_time-self.last_time))
self.last_time = next_time
# Character movements
right, upward, forward = 0, 0, 0
if self.is_pressed(glfw.KEY_UP): forward += 1
@ -205,22 +267,26 @@ class View:
self.context.viewport = 0, 0, width, height
self.context.clear(*color('Background'))
visibility = sqrt(1800 / self.fov)
visibility = self.visibility
projection = Matrix44.perspective_projection(self.fov, width/height,
3E-3, visibility)
view = Matrix44.look_at(self.pos, self.pos + self.forward, self.upward)
vp = (view @ projection).astype(np.float32).tobytes()
self.maprog['visibility'].write(np.float32(visibility).tobytes())
self.maprog['visibility'].write(visibility.tobytes())
self.maprog['camera'].write(self.pos.tobytes())
self.maprog['mvp'].write(vp)
self.mapva.render(moderngl.TRIANGLES)
self.prog['visibility'].write(np.float32(visibility).tobytes())
self.prog['visibility'].write(visibility.tobytes())
self.prog['camera'].write(self.pos.tobytes())
self.prog['vp'].write(vp)
for pico in self.picos.copy().values():
if pico is not self.camera: self.render_pico(pico)
self.shards = self.pool.map(Shard.update, self.shards)
for shard in self.shards:
#shard.update()
self.render_shard(shard)
glfw.swap_buffers(self.window)
# Resetting cursor position and event queues
@ -230,3 +296,4 @@ class View:
def close(self):
"""Close window."""
glfw.terminate()
self.pool.terminate()