Let shards do damage

This commit is contained in:
Nguyễn Gia Phong 2019-08-26 16:00:04 +07:00
parent 1ec17fa772
commit a7c92163c0
4 changed files with 92 additions and 75 deletions

View File

@ -22,7 +22,7 @@ __doc__ = 'Axuy main loop'
from argparse import ArgumentParser, RawTextHelpFormatter
from pickle import dumps, loads
from socket import socket, SOCK_DGRAM, SOL_SOCKET, SO_REUSEADDR
from threading import Event, RLock, Thread
from threading import Event, Thread
from .misc import mapgen, mapidgen, whilst
from .pico import Picobot
@ -39,7 +39,7 @@ class Peer:
self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
self.sock.bind((config.host, config.port))
self.addr = self.sock.getsockname()
self.updated, lock = Event(), RLock()
self.updated = Event()
if config.seeder is None:
mapid, self.peers = mapidgen(), []
@ -50,11 +50,11 @@ class Peer:
self.space = mapgen(mapid)
self.pico = Picobot(self.addr, self.space)
self.view = View(self.addr, self.pico, self.space, config, lock)
self.view = View(self.addr, self.pico, self.space, config)
Thread(target=self.serve, args=(mapid,), daemon=True).start()
Thread(target=self.push, daemon=True).start()
Thread(target=self.pull, args=(lock,), daemon=True).start()
Thread(target=self.pull, daemon=True).start()
def serve(self, mapid):
"""Initiate peers."""
@ -72,23 +72,23 @@ class Peer:
def push(self):
"""Send own state to peers."""
self.updated.wait()
pico = self.pico
shards = {i: (s.pos, s.rot, s.power)
for i, s in self.pico.shards.items()}
data = dumps([self.pico.pos, self.pico.rot, shards])
for i, s in pico.shards.items()}
data = dumps([pico.health, pico.pos, pico.rot, shards])
for peer in self.peers:
self.sock.sendto(data, peer)
self.updated.clear()
@whilst
def pull(self, lock):
def pull(self):
"""Receive peers' state."""
data, addr = self.sock.recvfrom(1<<16)
pos, rot, shards = loads(data)
with lock:
if addr not in self.view.picos:
self.peers.append(addr)
self.view.add_pico(addr, pos, rot)
self.view.picos[addr].sync(pos, rot, shards)
health, pos, rot, shards = loads(data)
if addr not in self.view.picos:
self.peers.append(addr)
self.view.add_pico(addr)
self.view.picos[addr].sync(health, pos, rot, shards)
def update(self):
"""Locally update the internal states."""

View File

@ -19,7 +19,7 @@
__doc__ = 'Axuy module for character class'
from itertools import combinations
from math import pi, sqrt
from math import log10, pi, sqrt
from random import random
import numpy as np
@ -33,6 +33,7 @@ OCTOVERTICES = np.float32([(a + b) / 2.0
for a, b in combinations(TETRAVERTICES, 2)])
RPICO = norm(TETRAVERTICES[0])
RSHARD = norm(OCTOVERTICES[0])
RCOLL = RPICO * 2/3
INVX = np.float32([[-1, 0, 0], [0, 1, 0], [0, 0, 1]])
INVY = np.float32([[1, 0, 0], [0, -1, 0], [0, 0, 1]])
@ -105,7 +106,7 @@ class Shard:
if z is None: z = self.z
return not placeable(self.space, x, y, z, r=RSHARD)
def update(self, fps):
def update(self, fps, picos):
"""Update states."""
x, y, z = self.pos + self.forward/fps*SHARD_SPEED
bounced = False
@ -121,6 +122,12 @@ class Shard:
self.pos += self.forward / fps * SHARD_SPEED
self.power -= bounced
for pico in picos:
distance = norm(pico.pos - self.pos)
if distance < RCOLL:
pico.health -= self.power * RCOLL
self.power = 0
def sync(self, position, rotation, power) -> None:
"""Synchronize state received from other peers."""
self.pos, self.rot, self.power = position, rotation, power
@ -136,6 +143,8 @@ class Picobot:
IP address (host, port).
space : np.ndarray of shape (12, 12, 9) of bools
3D array of occupied space.
health : float, optional
Pico relative health.
position : iterable of length 3 of floats, optional
Position.
rotation : np.ndarray of shape (3, 3) of np.float32, optional
@ -147,6 +156,8 @@ class Picobot:
IP address (host, port).
space : np.ndarray of shape (12, 12, 9) of bools
3D array of occupied space.
health : float
Pico relative health.
x, y, z : floats
Position.
rot : np.ndarray of shape (3, 3) of np.float32
@ -160,9 +171,11 @@ class Picobot:
fps : float
Currently rendered frames per second.
"""
def __init__(self, address, space, position=None, rotation=None):
def __init__(self, address, space,
health=1.0, position=None, rotation=None):
self.addr = address
self.space = space
self.health = health
if position is None:
x, y, z = random()*12, random()*12, random()*9
@ -183,7 +196,12 @@ class Picobot:
self.fps = 60.0
@property
def pos(self):
def dead(self) -> bool:
"""Whether the pico is dead."""
return self.health < 0
@property
def pos(self) -> np.float32:
"""Position in a NumPy array."""
return np.float32([self.x, self.y, self.z])
@ -191,9 +209,9 @@ class Picobot:
def pos(self, position):
self.x, self.y, self.z = position
def sync(self, position, rotation, shards) -> None:
def sync(self, health, position, rotation, shards):
"""Synchronize state received from other peers."""
self.pos, self.rot = position, rotation
self.health, self.pos, self.rot = health, position, rotation
for i, t in shards.items():
pos, rot, power = t
try:
@ -215,13 +233,12 @@ class Picobot:
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.
This is the equivalence of Shard.update
and should be called every loop.
"""
def update(self, right=0, upward=0, forward=0):
"""Recover health point and try to move in the given direction."""
if self.dead: return self.__init__(self.addr, self.space) # respawn
dt = 1.0 / self.fps
self.health = min(1.0, self.health + log10(self.health+1)*dt)
direction = normalized(right, upward, forward) @ self.rot
if self.recoil_t:
direction += self.recoil_u * self.recoil_t * RPS
@ -233,7 +250,7 @@ class Picobot:
def shoot(self):
"""Shoot in the forward direction."""
if self.recoil_t: return
if self.recoil_t or self.dead: return
self.recoil_t = 1.0 / RPS
self.recoil_u = [0, 0, -1] @ self.rot
self.shards[max(self.shards, default=0) + 1] = Shard(

View File

@ -2,7 +2,7 @@
#define SQR(x) ((x) * (x))
uniform float invfov;
uniform float abrtn;
uniform float zoom;
uniform sampler2D la;
uniform sampler2D tex;
@ -28,7 +28,7 @@ void main(void)
vec2 vert = text * 2.0 - 1.0;
f_color = texture(la, text) + vec4(
texture(tex, fringe(vert, -invfov)).r,
texture(tex, fringe(vert, invfov)).g,
texture(tex, fringe(vert, -abrtn)).r,
texture(tex, fringe(vert, abrtn)).g,
texture(tex, text).b, 1.0);
}

View File

@ -203,8 +203,6 @@ class View:
Vertical synchronization.
ctl : dict of (str, int)
Input control.
lock : RLock
Compound data lock to avoid size change during iteration.
Attributes
----------
@ -218,8 +216,6 @@ class View:
Enemies characters.
colors : dict of (address, str)
Color names of enemies.
lock : RLock
Compound data lock to avoid size change during iteration.
window : GLFW window
zmlvl : float
Zoom level (from ZMIN to ZMAX).
@ -233,12 +229,12 @@ class View:
Processed executable code in GLSL for map rendering.
mapva : moderngl.VertexArray
Vertex data of the map.
prog3 : moderngl.Program
prog : moderngl.Program
Processed executable code in GLSL
for rendering picobots and their shards.
pva3 : moderngl.VertexArray
pva : moderngl.VertexArray
Vertex data of picobots.
sva3 : moderngl.VertexArray
sva : moderngl.VertexArray
Vertex data of shards.
pfilter : moderngl.VertexArray
Vertex data for filtering highly saturated colors.
@ -246,7 +242,7 @@ class View:
Processed executable code in GLSL for Gaussian blur.
gausshva, gaussvva : moderngl.VertexArray
Vertex data for Gaussian blur.
fringe : moderngl.Program
edge : moderngl.Program
Processed executable code in GLSL for final combination
of the bloom effect with additional chromatic aberration
and barrel distortion.
@ -260,7 +256,7 @@ class View:
FPS during the last 5 seconds to display the average.
"""
def __init__(self, address, camera, space, config, lock):
def __init__(self, address, camera, space, config):
# Create GLFW window
if not glfw.init(): raise RuntimeError('Failed to initialize GLFW')
glfw.window_hint(glfw.CLIENT_API, glfw.OPENGL_API)
@ -284,7 +280,6 @@ class View:
self.picos = {address: camera}
self.colors = {address: randint(0, 5)}
self.last_time = glfw.get_time()
self.lock = lock
# Window's rendering and event-handling configuration
glfw.set_window_icon(self.window, 1, Image.open(abspath('icon.png')))
@ -332,11 +327,11 @@ class View:
svb = [(context.buffer(OCTOVERTICES.tobytes()), '3f', 'in_vert')]
sib = context.buffer(OCTOINDECIES.tobytes())
self.prog3 = context.program(vertex_shader=PICO_VERTEX,
geometry_shader=TRIANGLE_GEOMETRY,
fragment_shader=WORLD_FRAGMENT)
self.pva3 = context.vertex_array(self.prog3, pvb, pib)
self.sva3 = context.vertex_array(self.prog3, svb, sib)
self.prog = context.program(vertex_shader=PICO_VERTEX,
geometry_shader=TRIANGLE_GEOMETRY,
fragment_shader=WORLD_FRAGMENT)
self.pva = context.vertex_array(self.prog, pvb, pib)
self.sva = context.vertex_array(self.prog, svb, sib)
self.pfilter = context.simple_vertex_array(
context.program(vertex_shader=TEX_VERTEX,
@ -352,12 +347,12 @@ class View:
self.gaussv['height'].value = 256 * height / width
self.gaussvva = context.simple_vertex_array(
self.gaussv, context.buffer(QUAD), 'in_vert')
self.fringe = context.program(vertex_shader=TEX_VERTEX,
fragment_shader=COMBINE_FRAGMENT)
self.fringe['la'].value = 0
self.fringe['tex'].value = 1
self.edge = context.program(vertex_shader=TEX_VERTEX,
fragment_shader=COMBINE_FRAGMENT)
self.edge['la'].value = 0
self.edge['tex'].value = 1
self.combine = context.simple_vertex_array(
self.fringe, context.buffer(QUAD), 'in_vert')
self.edge, context.buffer(QUAD), 'in_vert')
size, table = (width, height), (256, height * 256 // width)
self.fb = context.framebuffer(context.texture(size, 4),
@ -417,6 +412,11 @@ class View:
"""Viewport height."""
return self.context.viewport[3]
@property
def health(self) -> float:
"""Camera relative health point."""
return self.camera.health
@property
def pos(self) -> np.float32:
"""Camera position in a NumPy array."""
@ -483,27 +483,27 @@ class View:
"""Return whether given keys are pressed."""
return any(glfw.get_key(self.window, k) == glfw.PRESS for k in keys)
def prender(self, obj, va3, col, bright=1.0):
def prender(self, obj, va, col, bright):
"""Render the obj and its images in bounded 3D space."""
rotation = Matrix44.from_matrix33(obj.rot).astype(np.float32).tobytes()
position = obj.pos.astype(np.float32).tobytes()
self.prog3['rot'].write(rotation)
self.prog3['pos'].write(position)
self.prog3['color'].write(color(col, bright).tobytes())
va3.render(moderngl.TRIANGLES)
self.prog['rot'].write(rotation)
self.prog['pos'].write(position)
self.prog['color'].write(color(col, bright).tobytes())
va.render(moderngl.TRIANGLES)
def render_pico(self, pico):
"""Render pico and its images in bounded 3D space."""
self.prender(pico, self.pva3, self.colors[pico.addr])
self.prender(pico, self.pva, self.colors[pico.addr], pico.health)
def render_shard(self, shard):
"""Render shard and its images in bounded 3D space."""
self.prender(shard, self.sva3,
self.prender(shard, self.sva,
self.colors[shard.addr], shard.power/SHARD_LIFE)
def add_pico(self, address, position, rotation):
"""Add picobot from addr at pos with rot."""
self.picos[address] = Picobot(address, self.space, position, rotation)
def add_pico(self, address):
"""Add picobot from given address."""
self.picos[address] = Picobot(address, self.space)
self.colors[address] = randint(0, 5)
def render(self):
@ -521,20 +521,20 @@ class View:
self.mapva.render(moderngl.TRIANGLES)
# Render picos and shards
self.prog3['visibility'].value = visibility
self.prog3['camera'].write(self.pos.tobytes())
self.prog3['vp'].write(vp)
self.prog['visibility'].value = visibility
self.prog['camera'].write(self.pos.tobytes())
self.prog['vp'].write(vp)
with self.lock:
for pico in self.picos.values():
shards = {}
for index, shard in pico.shards.items():
if not shard.power: continue
shard.update(self.fps)
self.render_shard(shard)
shards[index] = shard
pico.shards = shards
if pico is not self.camera: self.render_pico(pico)
picos = list(self.picos.values())
for pico in picos:
shards = {}
for index, shard in pico.shards.items():
shard.update(self.fps, picos)
if not shard.power: continue
self.render_shard(shard)
shards[index] = shard
pico.shards = shards
if pico is not self.camera: self.render_pico(pico)
def update(self):
"""Handle input, update GLSL programs and render the map."""
@ -549,7 +549,7 @@ class View:
if self.is_pressed(self.key['backward']): forward -= 1
if self.is_pressed(self.key['left']): right -= 1
if self.is_pressed(self.key['right']): right += 1
self.camera.move(right, upward, forward)
self.camera.update(right, upward, forward)
glfw.set_window_title(self.window, '{} - axuy@{}:{} ({})'.format(
self.postr, *self.addr, self.fpstr))
@ -573,8 +573,8 @@ class View:
self.context.screen.use()
self.context.clear()
self.fringe['invfov'].value = 1.0 / self.fov**CONWAY
self.fringe['zoom'].value = (self.zmlvl + 1.0) / 100
self.edge['abrtn'].value = (self.fov*self.health) ** -CONWAY
self.edge['zoom'].value = (self.zmlvl + 1.0) / 100
self.combine.render(moderngl.TRIANGLES)
glfw.swap_buffers(self.window)
glfw.poll_events()