[engine] use signals

This commit is contained in:
2018-03-05 15:06:31 +03:00
parent 73c46d821e
commit 5330be142c
10 changed files with 147 additions and 51 deletions

View File

@@ -0,0 +1,19 @@
package ru.m.tankz.control;
import haxework.provider.Provider;
import ru.m.tankz.network.NetworkManager;
import ru.m.tankz.control.Control;
class ClientNetworkControl extends HumanControl {
private var network(get, never):NetworkManager;
inline private function get_network():NetworkManager {
return Provider.get(NetworkManager);
}
override public function action(action:TankAction):Void {
network.action(action);
}
}

View File

@@ -42,11 +42,10 @@ class GameFrame extends VGroupView implements ViewBuilder implements GameFrameLa
if (game == null) { if (game == null) {
throw 'Unsupported game type "${save.state.type}"'; throw 'Unsupported game type "${save.state.type}"';
} }
game.engine.listeners.push(render); game.engine.connect(render);
game.engine.listeners.push(Provider.get(SoundManager)); game.engine.connect(Provider.get(SoundManager));
game.start(save).then(onGameStateChange).endThen(onGameComplete); game.start(save).then(onGameStateChange).endThen(onGameComplete);
content.addEventListener(Event.ENTER_FRAME, redraw); content.addEventListener(Event.ENTER_FRAME, redraw);
//Provider.get(IConnection).packetHandler.addListener(this);
render.draw(game.engine); render.draw(game.engine);
timer = new Timer(10); timer = new Timer(10);
timer.run = updateEngine; timer.run = updateEngine;
@@ -55,7 +54,6 @@ class GameFrame extends VGroupView implements ViewBuilder implements GameFrameLa
} }
private function stop():Void { private function stop():Void {
//Provider.get(IConnection).packetHandler.removeListener(this);
if (timer != null) { if (timer != null) {
timer.stop(); timer.stop();
timer = null; timer = null;

View File

@@ -1,5 +1,9 @@
package ru.m.tankz.network; package ru.m.tankz.network;
import ru.m.tankz.proto.game.GameChange;
import ru.m.tankz.proto.game.GameActionType;
import ru.m.tankz.proto.pack.GameUpdateRequest;
import ru.m.tankz.control.Control;
import ru.m.tankz.proto.pack.JoinGameRequest; import ru.m.tankz.proto.pack.JoinGameRequest;
import ru.m.tankz.proto.pack.LeaveGameRequest; import ru.m.tankz.proto.pack.LeaveGameRequest;
import ru.m.tankz.proto.pack.CreateGameRequest; import ru.m.tankz.proto.pack.CreateGameRequest;
@@ -22,6 +26,7 @@ class NetworkManager {
public var stateSignal:Signal<String>; public var stateSignal:Signal<String>;
public var listGameSignal:Signal<Array<Game>>; public var listGameSignal:Signal<Array<Game>>;
public var gameSignal:Signal<Game>; public var gameSignal:Signal<Game>;
public var gameUpdateSignal:Signal<Array<GameChange>>;
public var user(default, null):User; public var user(default, null):User;
private var connection(get, never):ClientConnection; private var connection(get, never):ClientConnection;
@@ -39,6 +44,7 @@ class NetworkManager {
stateSignal = new Signal<String>(); stateSignal = new Signal<String>();
listGameSignal = new Signal<Array<Game>>(); listGameSignal = new Signal<Array<Game>>();
gameSignal = new Signal<Game>(); gameSignal = new Signal<Game>();
gameUpdateSignal = new Signal<Array<GameChange>>();
updateState('offline'); updateState('offline');
connection.handler.connect(onConnectionEvent); connection.handler.connect(onConnectionEvent);
connection.receiveHandler.connect(onResponse); connection.receiveHandler.connect(onResponse);
@@ -82,6 +88,18 @@ class NetworkManager {
connection.send(new Request().setLeaveGame(new LeaveGameRequest())); connection.send(new Request().setLeaveGame(new LeaveGameRequest()));
} }
public function action(action:TankAction):Void {
var update:GameUpdateRequest = switch action {
case TankAction.MOVE(direction): new GameUpdateRequest().setType(GameActionType.MOVE).setDirectionX(direction.x).setDirectionY(direction.y);
case TankAction.STOP: new GameUpdateRequest().setType(GameActionType.STOP);
case TankAction.SHOT: new GameUpdateRequest().setType(GameActionType.SHOT);
case _: null;
}
if (update != null) {
connection.send(new Request().setUpdateGame(update));
}
}
private function onConnectionEvent(event:ConnectionEvent):Void { private function onConnectionEvent(event:ConnectionEvent):Void {
L.d('Network', '${event}'); L.d('Network', '${event}');
updateState(switch (event) { updateState(switch (event) {
@@ -110,6 +128,8 @@ class NetworkManager {
gameSignal.emit(packet.joinGame.game); gameSignal.emit(packet.joinGame.game);
} else if (packet.hasLeaveGame()) { } else if (packet.hasLeaveGame()) {
gameSignal.emit(null); gameSignal.emit(null);
}else if (packet.hasUpdateGame()) {
gameUpdateSignal.emit(packet.updateGame.changes);
} }
} }
} }

View File

@@ -12,7 +12,7 @@ import ru.m.tankz.engine.Engine;
import ru.m.tankz.render.RenderItem; import ru.m.tankz.render.RenderItem;
class Render extends SpriteView implements EngineListener { class Render extends SpriteView {
private var backgroundLayer:Sprite; private var backgroundLayer:Sprite;
private var groundLayer:Sprite; private var groundLayer:Sprite;
@@ -126,7 +126,7 @@ class Render extends SpriteView implements EngineListener {
} }
} }
public function onChange(entity:EntityType, ?change:EntityChange):Void {} public function onChange(entity:EntityType, change:EntityChange):Void {}
public function onCollision(entity:EntityType, with:EntityType):Void { public function onCollision(entity:EntityType, with:EntityType):Void {
switch [entity, with] { switch [entity, with] {

View File

@@ -1,12 +1,12 @@
package ru.m.tankz.sound; package ru.m.tankz.sound;
import ru.m.tankz.core.EntityType;
import ru.m.tankz.engine.Engine; import ru.m.tankz.engine.Engine;
import openfl.media.Sound; import openfl.media.Sound;
import openfl.utils.Assets; import openfl.utils.Assets;
import ru.m.tankz.core.EntityType;
class SoundManager implements EngineListener { class SoundManager {
private static var TAG(default, never):String = 'SoundManager'; private static var TAG(default, never):String = 'SoundManager';
#if flash #if flash
@@ -37,12 +37,12 @@ class SoundManager implements EngineListener {
} }
} }
public function onChange(entity:EntityType, ?change:EntityChange):Void { public function onChange(entity:EntityType, change:EntityChange):Void {
switch [entity, change] { switch [entity, change] {
case [EntityType.TANK(_), EntityChange.HIT]: case [EntityType.TANK(_), EntityChange.HIT]:
play('bullet_hit'); play('bullet_hit');
case [EntityType.TANK(_), EntityChange.LIVE_UP]: //case [EntityType.TANK(_), EntityChange.LIVE_UP]:
play('live'); // play('live');
case [EntityType.EAGLE(_), EntityChange.DEATH]: case [EntityType.EAGLE(_), EntityChange.DEATH]:
play('boom_player'); play('boom_player');
case _: case _:

View File

@@ -1,27 +1,53 @@
package ru.m.signal; package ru.m.signal;
typedef Signal<A> = Signal1<A>;
typedef Receiver<T> = T -> Void; typedef Receiver1<A> = A -> Void;
class Signal<T> { class Signal1<A> {
private var receivers:Array<Receiver<T>>; private var receivers:Array<Receiver1<A>>;
public function new() { public function new() {
receivers = []; receivers = [];
} }
public function connect(receiver:Receiver<T>):Void { public function connect(receiver:Receiver1<A>):Void {
receivers.push(receiver); receivers.push(receiver);
} }
public function disconnect(receiver:Receiver<T>):Void { public function disconnect(receiver:Receiver1<A>):Void {
receivers.remove(receiver); receivers.remove(receiver);
} }
public function emit(value:T):Void { public function emit(a:A):Void {
for (receiver in receivers) { for (receiver in receivers) {
receiver(value); receiver(a);
}
}
}
typedef Receiver2<A, B> = A -> B -> Void;
class Signal2<A, B> {
private var receivers:Array<Receiver2<A, B>>;
public function new() {
receivers = [];
}
public function connect(receiver:Receiver2<A, B>):Void {
receivers.push(receiver);
}
public function disconnect(receiver:Receiver2<A, B>):Void {
receivers.remove(receiver);
}
public function emit(a:A, b:B):Void {
for (receiver in receivers) {
receiver(a, b);
} }
} }
} }

View File

@@ -13,7 +13,6 @@ enum TankAction {
SHOT; SHOT;
} }
class Control { class Control {
public static var NONE(default, never):ControlType = 'none'; public static var NONE(default, never):ControlType = 'none';
public static var HUMAN(default, never):ControlType = 'human'; public static var HUMAN(default, never):ControlType = 'human';

View File

@@ -1,5 +1,6 @@
package ru.m.tankz.engine; package ru.m.tankz.engine;
import ru.m.signal.Signal;
import ru.m.geom.Line; import ru.m.geom.Line;
import ru.m.geom.Point; import ru.m.geom.Point;
import ru.m.tankz.config.Config; import ru.m.tankz.config.Config;
@@ -13,32 +14,29 @@ import ru.m.tankz.map.LevelMap;
enum EntityChange { enum EntityChange {
DEATH;
HIT; HIT;
LIVE_UP;
TYPE; TYPE;
DEATH;
PROTECT;
FREEZING;
} }
typedef EngineListener = {
interface EngineListener {
public function onSpawn(entity:EntityType):Void; public function onSpawn(entity:EntityType):Void;
public function onChange(entity:EntityType, ?change:EntityChange):Void;
public function onCollision(entity:EntityType, with:EntityType):Void; public function onCollision(entity:EntityType, with:EntityType):Void;
public function onDestroy(entity:EntityType):Void; public function onDestroy(entity:EntityType):Void;
public function onChange(entity:EntityType, change:EntityChange):Void;
} }
class CollisionProcessor implements EngineListener { class CollisionProcessor {
private var engine:Engine; private var engine:Engine;
public function new(engine:Engine) { public function new(engine:Engine) {
this.engine = engine; this.engine = engine;
engine.collisionSignal.connect(onCollision);
} }
public function onSpawn(entity:EntityType):Void {}
public function onChange(entity:EntityType, ?change:EntityChange):Void {}
public function onCollision(entity:EntityType, with:EntityType):Void { public function onCollision(entity:EntityType, with:EntityType):Void {
switch [entity, with] { switch [entity, with] {
case [EntityType.TANK(tank), EntityType.TANK(other_tank)]: case [EntityType.TANK(tank), EntityType.TANK(other_tank)]:
@@ -74,47 +72,75 @@ class CollisionProcessor implements EngineListener {
engine.destroy(bullet); engine.destroy(bullet);
if (!eagle.protect.active) { if (!eagle.protect.active) {
eagle.death = true; eagle.death = true;
// ToDo: change
engine.change(eagle, EntityChange.DEATH); engine.change(eagle, EntityChange.DEATH);
} }
case _: case _:
} }
} }
public function onDestroy(entity:EntityType):Void { } public function dispose():Void {
if (engine != null) {
engine.collisionSignal.disconnect(onCollision);
engine = null;
}
}
}
class EngineDispatcher {
public var spawnSignal(default, null):Signal1<EntityType>;
public var collisionSignal(default, null):Signal2<EntityType, EntityType>;
public var destroySignal(default, null):Signal1<EntityType>;
public var changeSignal(default, null):Signal2<EntityType, EntityChange>;
public function new() {
spawnSignal = new Signal1<EntityType>();
collisionSignal = new Signal2<EntityType, EntityType>();
destroySignal = new Signal1<EntityType>();
changeSignal = new Signal2<EntityType, EntityChange>();
}
public function connect(listener:EngineListener) {
spawnSignal.connect(listener.onSpawn);
collisionSignal.connect(listener.onCollision);
destroySignal.connect(listener.onDestroy);
changeSignal.connect(listener.onChange);
}
public function disconnect(listener:EngineListener) {
spawnSignal.disconnect(listener.onSpawn);
collisionSignal.disconnect(listener.onCollision);
destroySignal.disconnect(listener.onDestroy);
changeSignal.disconnect(listener.onChange);
}
} }
@:yield @:yield
class Engine implements ControlHandler { class Engine extends EngineDispatcher implements ControlHandler {
public var config(default, default):Config; public var config(default, default):Config;
public var map(default, null):LevelMap; public var map(default, null):LevelMap;
public var entities(default, null):Map<Int, Entity>; public var entities(default, null):Map<Int, Entity>;
public var listeners(default, null):Array<EngineListener>;
private var collision:CollisionProcessor; private var collision:CollisionProcessor;
private var time:Float; private var time:Float;
public function new(config:Config) { public function new(config:Config) {
super();
this.config = config; this.config = config;
listeners = [];
map = new LevelMap(config.map); map = new LevelMap(config.map);
entities = new Map<Int, Entity>(); entities = new Map<Int, Entity>();
time = Date.now().getTime(); time = Date.now().getTime();
collision = new CollisionProcessor(this); collision = new CollisionProcessor(this);
listeners.push(collision);
} }
public function spawn(entity:Entity):Void { public function spawn(entity:Entity):Void {
entities.set(entity.id, entity); entities.set(entity.id, entity);
var type = EntityTypeResolver.of(entity); var type = EntityTypeResolver.of(entity);
for (l in listeners) l.onSpawn(type); spawnSignal.emit(type);
}
public function change(entity:Entity, ?change:EntityChange):Void {
var type = EntityTypeResolver.of(entity);
for (l in listeners) l.onChange(type, change);
} }
public function destroy(entity:Entity):Void { public function destroy(entity:Entity):Void {
@@ -126,7 +152,7 @@ class Engine implements ControlHandler {
if (tank != null) tank.onDestroyBullet(); if (tank != null) tank.onDestroyBullet();
case _: case _:
} }
for (l in listeners) l.onDestroy(type); destroySignal.emit(type);
entities.remove(entity.id); entities.remove(entity.id);
} }
} }
@@ -152,6 +178,11 @@ class Engine implements ControlHandler {
} }
} }
public function change(entity:Entity, change:EntityChange):Void {
var type = EntityTypeResolver.of(entity);
changeSignal.emit(type, change);
}
public function update():Void { public function update():Void {
var newTime:Float = Date.now().getTime(); var newTime:Float = Date.now().getTime();
var d:Float = newTime - time; var d:Float = newTime - time;
@@ -197,7 +228,7 @@ class Engine implements ControlHandler {
entity.rect.lean(cell.rect); entity.rect.lean(cell.rect);
collision = true; collision = true;
var with = EntityTypeResolver.of(cell); var with = EntityTypeResolver.of(cell);
for (l in listeners) l.onCollision(entityType, with); collisionSignal.emit(entityType, with);
isStop = true; isStop = true;
break; break;
} }
@@ -207,7 +238,7 @@ class Engine implements ControlHandler {
if (other != ent && other != null) { if (other != ent && other != null) {
if (other.rect.intersection2(side)) { if (other.rect.intersection2(side)) {
var with = EntityTypeResolver.of(other); var with = EntityTypeResolver.of(other);
for (l in listeners) l.onCollision(entityType, with); collisionSignal.emit(entityType, with);
isStop = true; isStop = true;
} }
} }
@@ -241,7 +272,10 @@ class Engine implements ControlHandler {
} }
public function dispose():Void { public function dispose():Void {
listeners = []; if (collision != null) {
collision.dispose();
collision = null;
}
entities = new Map(); entities = new Map();
} }

View File

@@ -24,7 +24,7 @@ import ru.m.tankz.game.Spawner;
import ru.m.tankz.Type; import ru.m.tankz.Type;
class Game implements EngineListener { class Game {
private static var TAG(default, never):String = 'Game'; private static var TAG(default, never):String = 'Game';
@@ -43,7 +43,7 @@ class Game implements EngineListener {
this.type = type; this.type = type;
this.config = Provider.get(IConfigBundle).get(type); this.config = Provider.get(IConfigBundle).get(type);
this.engine = new Engine(config); this.engine = new Engine(config);
engine.listeners.push(this); engine.connect(this);
} }
public function getTeam(teamId:TeamId):Team { public function getTeam(teamId:TeamId):Team {
@@ -160,7 +160,7 @@ class Game implements EngineListener {
} }
} }
public function onChange(entity:EntityType, ?change:EntityChange):Void { public function onChange(entity:EntityType, change:EntityChange):Void {
switch [entity, change] { switch [entity, change] {
case [EntityType.EAGLE(eagle), EntityChange.DEATH]: case [EntityType.EAGLE(eagle), EntityChange.DEATH]:
if (eagle.death) { if (eagle.death) {
@@ -264,11 +264,11 @@ class Game implements EngineListener {
} }
case 'helmet': case 'helmet':
tank.protect.on(bonus.config.duration); tank.protect.on(bonus.config.duration);
engine.change(tank); engine.change(tank, EntityChange.PROTECT);
case 'clock': case 'clock':
for (t in engine.iterTanks(alienTank(tank.playerId.team))) { for (t in engine.iterTanks(alienTank(tank.playerId.team))) {
t.freezing.on(bonus.config.duration); t.freezing.on(bonus.config.duration);
engine.change(t); engine.change(t, EntityChange.FREEZING);
} }
case 'shovel': case 'shovel':
// ToDo: protect eagle/area // ToDo: protect eagle/area
@@ -276,7 +276,7 @@ class Game implements EngineListener {
if (team.eagleId > 0) { if (team.eagleId > 0) {
var eagle:Eagle = cast(engine.entities[team.eagleId], Eagle); var eagle:Eagle = cast(engine.entities[team.eagleId], Eagle);
eagle.protect.on(bonus.config.duration); eagle.protect.on(bonus.config.duration);
engine.change(eagle); engine.change(eagle, EntityChange.PROTECT);
} }
case _: case _:
engine.destroy(tank); // :-D engine.destroy(tank); // :-D

View File

@@ -100,6 +100,6 @@ message Response {
JoinGameResponse joinGame = 5; JoinGameResponse joinGame = 5;
LeaveGameResponse leaveGame = 6; LeaveGameResponse leaveGame = 6;
StartGameResponse startGame = 7; StartGameResponse startGame = 7;
GameUpdateResponse udpateGame = 8; GameUpdateResponse updateGame = 8;
} }
} }