Dead Reckoning (推測航法)
ネットワークゲームの定番のアルゴリズムの一つだそうな。
http://www.pyramid-inc.net/lab/archives/103
灰色: 自分自身の実際の座標
白色: ネットワーク越しの他プレイヤーから見えるであろう座標
/**
* Copyright o8que ( http://wonderfl.net/user/o8que )
* MIT License ( http://www.opensource.org/licenses/mit-license.php )
* Downloaded from: http://wonderfl.net/c/wNZt
*/
package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.utils.Timer;
import net.user1.reactor.Reactor;
import net.user1.reactor.ReactorEvent;
import net.user1.reactor.Room;
import net.user1.reactor.RoomEvent;
import net.wonderfl.utils.WonderflAPI;
[SWF(width = "465", height = "465", frameRate = "60", backgroundColor = "0x000000")]
public class Main extends Sprite {
private var _reactor:Reactor;
private var _room:Room;
private var _player:Player;
private var _dro:DeadReckoningObject;
public static const PLAYER_MAX_SPEED:Number = 3;
public static const PLAYER_SEND_INTERVAL:int = 125; // プレイヤー状態の送信間隔(ms)
public static const DRO_COVERGENCE_PERIOD:int = 375; // 補間の収束期間(ms)
public static const PING:int = 200; // 遅延のシミュレート(ms)
public function Main() {
_reactor = new Reactor();
_reactor.addEventListener(ReactorEvent.READY, initialize);
_reactor.connect("tryunion.com", 80);
}
private function initialize(event:ReactorEvent):void {
_reactor.getServer().syncTime();
_room = _reactor.getRoomManager().createRoom(String(new WonderflAPI(root.loaderInfo.parameters).appID));
_room.join();
_player = new Player(_reactor, stage.frameRate);
_dro = new DeadReckoningObject(232.5, 232.5, DRO_COVERGENCE_PERIOD);
_dro.update(_reactor.getServer().getServerTime(), "232,232,0,0," + _reactor.getServer().getServerTime());
_room.addEventListener(RoomEvent.UPDATE_CLIENT_ATTRIBUTE, updateDRO);
addChild(new UnionStats(_reactor));
addEventListener(Event.ENTER_FRAME, update);
function updateDRO(event:RoomEvent):void {
var timer:Timer = new Timer(PING, 1);
var state:String = _reactor.self().getAttribute("s");
timer.addEventListener(TimerEvent.TIMER_COMPLETE, function():void {
_dro.update(_reactor.getServer().getServerTime(), state);
});
timer.start();
}
}
private function update(event:Event):void {
_player.update(mouseX, mouseY);
_dro.move(_reactor.getServer().getServerTime());
graphics.clear();
graphics.beginFill(0x000000);
graphics.drawRect(0, 0, 465, 465);
graphics.endFill();
graphics.beginFill(0x444444);
graphics.drawCircle(_player.x, _player.y, 16);
graphics.endFill();
graphics.beginFill(0xFFFFFF);
graphics.drawCircle(_dro.x, _dro.y, 16);
graphics.endFill();
}
}
}
/* ------------------------------------------------------------------------------------------------
* Player
* ------------------------------------------------------------------------------------------------
*/
//package {
import flash.geom.Point;
import net.user1.reactor.Reactor;
//public
class Player {
private var _reactor:Reactor;
private var _frameRate:int;
private var _position:Point;
private var _velocity:Point;
private var _lastSentTime:Number;
public function Player(reactor:Reactor, frameRate:int) {
_reactor = reactor;
_frameRate = frameRate;
_position = new Point(232.5, 232.5);
_velocity = new Point(0, 0);
_lastSentTime = 0;
sendState();
}
private function sendState():void {
_lastSentTime = _reactor.getServer().getServerTime();
_reactor.self().setAttribute("s",
int(_position.x) + "," + int(_position.y) + ","
+ int(_velocity.x * _frameRate) + "," + int(_velocity.y * _frameRate) + ","
+ _lastSentTime
);
}
public function update(mouseX:Number, mouseY:Number):void {
_velocity.x = mouseX - _position.x;
_velocity.y = mouseY - _position.y;
if (_velocity.length > Main.PLAYER_MAX_SPEED) { _velocity.normalize(Main.PLAYER_MAX_SPEED); }
_position.x += _velocity.x;
_position.y += _velocity.y;
if (_reactor.getServer().getServerTime() - _lastSentTime > Main.PLAYER_SEND_INTERVAL) { sendState(); }
}
public function get x():Number { return _position.x; }
public function get y():Number { return _position.y; }
}
//}
/* ------------------------------------------------------------------------------------------------
* DeadReckoningObject
* ------------------------------------------------------------------------------------------------
*/
//package {
import flash.geom.Point;
//public
class DeadReckoningObject {
private var _currentPosition:Point;
private var _previousPosition:Point;
private var _timestamp:Number;
private var _attributePosition:Point;
private var _attributeVelocity:Point;
private var _attributeTimestamp:Number;
private var _convergencePeriod:int;
private var _splinePoint0:Point;
private var _splinePoint1:Point;
private var _splinePoint2:Point;
private var _splinePoint3:Point;
public function DeadReckoningObject(x:Number, y:Number, convergencePeriod:int = 500) {
_currentPosition = new Point(x, y);
_previousPosition = new Point(x, y);
_timestamp = 0;
_attributePosition = new Point(0, 0);
_attributeVelocity = new Point(0, 0);
_attributeTimestamp = 0;
_convergencePeriod = convergencePeriod;
_splinePoint0 = new Point(0, 0);
_splinePoint1 = new Point(0, 0);
_splinePoint2 = new Point(0, 0);
_splinePoint3 = new Point(0, 0);
}
public function update(currentTime:Number, state:String):void {
_timestamp = currentTime;
var attributes:Array = state.split(",");
_attributePosition.x = Number(attributes[0]);
_attributePosition.y = Number(attributes[1]);
_attributeVelocity.x = Number(attributes[2]);
_attributeVelocity.y = Number(attributes[3]);
_attributeTimestamp = Number(attributes[4]);
_splinePoint0.x = _previousPosition.x;
_splinePoint0.y = _previousPosition.y;
_splinePoint1.x = _currentPosition.x;
_splinePoint1.y = _currentPosition.y;
var targetTime:Number = ((currentTime + _convergencePeriod) - _attributeTimestamp) / 1000;
_splinePoint2.x = _attributePosition.x + targetTime * _attributeVelocity.x;
_splinePoint2.y = _attributePosition.y + targetTime * _attributeVelocity.y;
_splinePoint3.x = _splinePoint2.x + 0.0625 * _attributeVelocity.x;
_splinePoint3.y = _splinePoint2.y + 0.0625 * _attributeVelocity.y;
}
public function move(currentTime:Number):void {
_previousPosition.x = _currentPosition.x;
_previousPosition.y = _currentPosition.y;
var elapsed:Number;
if (currentTime - _timestamp < _convergencePeriod) {
elapsed = (currentTime - _timestamp) / _convergencePeriod;
_currentPosition.x = calculateSplineValue(_splinePoint0.x, _splinePoint1.x, _splinePoint2.x, _splinePoint3.x, elapsed);
_currentPosition.y = calculateSplineValue(_splinePoint0.y, _splinePoint1.y, _splinePoint2.y, _splinePoint3.y, elapsed);
}else {
elapsed = (currentTime - _attributeTimestamp) / 1000;
_currentPosition.x = _attributePosition.x + elapsed * _attributeVelocity.x;
_currentPosition.y = _attributePosition.y + elapsed * _attributeVelocity.y;
}
}
// Catmull-Rom spline interpolation
private function calculateSplineValue(n0:Number, n1:Number, n2:Number, n3:Number, t:Number):Number {
return 0.5 * (t * (t * (t * ( -n0 + 3 * n1 - 3 * n2 + n3) + (2 * n0 - 5 * n1 + 4 * n2 - n3)) + ( -n0 + n2)) + 2 * n1);
}
public function get x():Number { return _currentPosition.x; }
public function get y():Number { return _currentPosition.y; }
}
//}
/* ------------------------------------------------------------------------------------------------
* UnionStats
* ------------------------------------------------------------------------------------------------
*/
//package {
import flash.display.Sprite;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.text.TextField;
import flash.text.TextFieldAutoSize;
import flash.text.TextFormat;
import flash.utils.Timer;
import net.user1.reactor.Reactor;
import net.user1.reactor.Statistics;
//public
class UnionStats extends Sprite {
private var _reactor:Reactor;
private var _fpsCount:int;
private var _timer:Timer;
private var _fps:TextField, _stageFps:TextField;
private var _mem:TextField, _peakMem:TextField;
private var _ping:TextField;
private var _recv:TextField, _peakRecv:TextField;
private var _send:TextField, _peakSend:TextField;
private var _msg:TextField, _peakMsg:TextField;
public function UnionStats(reactor:Reactor) {
_reactor = reactor;
_reactor.enableStatistics();
_fpsCount = 0;
addEventListener(Event.ENTER_FRAME, function(event:Event):void { _fpsCount++; } );
_timer = new Timer(1000, 0);
_timer.addEventListener(TimerEvent.TIMER, update);
_timer.start();
graphics.beginFill(0x404040);
graphics.drawRect(0, 0, 100, 100);
graphics.endFill();
graphics.lineStyle(1, 0xFFFFFF, 1, true, "normal", "none", "bevel");
graphics.moveTo(5, 34);
graphics.lineTo(95, 34);
addChild(createTextField("FPS ", 0, 0, 30, TextFieldAutoSize.LEFT));
addChild(createTextField("Mem ", 0, 16, 30, TextFieldAutoSize.LEFT));
addChild(createTextField("Ping ", 0, 36, 30, TextFieldAutoSize.LEFT));
addChild(createTextField("Down ", 0, 52, 30, TextFieldAutoSize.LEFT));
addChild(createTextField("Up ", 0, 68, 30, TextFieldAutoSize.LEFT));
addChild(createTextField("Msg ", 0, 84, 30, TextFieldAutoSize.LEFT));
addChild(_fps = createTextField("0", 30, 0, 30, TextFieldAutoSize.RIGHT));
addChild(_mem = createTextField("0", 30, 16, 30, TextFieldAutoSize.RIGHT));
addChild(_recv = createTextField("0", 30, 52, 30, TextFieldAutoSize.RIGHT));
addChild(_send = createTextField("0", 30, 68, 30, TextFieldAutoSize.RIGHT));
addChild(_msg = createTextField("0", 30, 84, 30, TextFieldAutoSize.RIGHT));
addChild(createTextField("/", 60, 0, 7, TextFieldAutoSize.CENTER));
addChild(createTextField("/", 60, 16, 7, TextFieldAutoSize.CENTER));
addChild(_ping = createTextField("0", 30, 36, 70, TextFieldAutoSize.CENTER));
addChild(createTextField("/", 60, 52, 7, TextFieldAutoSize.CENTER));
addChild(createTextField("/", 60, 68, 7, TextFieldAutoSize.CENTER));
addChild(createTextField("/", 60, 84, 7, TextFieldAutoSize.CENTER));
addChild(_stageFps = createTextField("0", 67, 0, 30, TextFieldAutoSize.LEFT));
addChild(_peakMem = createTextField("0", 67, 16, 30, TextFieldAutoSize.LEFT));
addChild(_peakRecv = createTextField("0", 67, 52, 30, TextFieldAutoSize.LEFT));
addChild(_peakSend = createTextField("0", 67, 68, 30, TextFieldAutoSize.LEFT));
addChild(_peakMsg = createTextField("0", 67, 84, 30, TextFieldAutoSize.LEFT));
}
private function update(event:TimerEvent):void {
_fps.text = _fpsCount.toString();
_fpsCount = 0;
if (stage) { _stageFps.text = stage.frameRate.toString(); }
if (!_reactor.isReady()) { return; }
_ping.text = _reactor.self().getPing().toString();
var stats:Statistics = _reactor.getStatistics();
_mem.text = stats.getTotalMemoryMB().toFixed(2);
_recv.text = stats.getKBReceivedPerSecond().toFixed(2);
_send.text = stats.getKBSentPerSecond().toFixed(2);
_msg.text = stats.getMessagesPerSecond().toString();
_peakMem.text = stats.getPeakMemoryMB().toFixed(2);
_peakRecv.text = stats.getPeakKBReceivedPerSecond().toFixed(2);
_peakSend.text = stats.getPeakKBSentPerSecond().toFixed(2);
_peakMsg.text = stats.getPeakMessagesPerSecond().toString();
}
private function createTextField(text:String, x:int, y:int, width:int, autoSize:String):TextField {
var result:TextField = new TextField();
result.x = x;
result.width = width;
result.autoSize = autoSize;
result.defaultTextFormat = new TextFormat("_sans", 9, 0xFFFFFF, true);
result.text = text;
result.y = y;
result.mouseEnabled = result.selectable = false;
return result;
}
}
//}