Last active
August 29, 2015 14:07
-
-
Save edolganov/597090d81398b6f58586 to your computer and use it in GitHub Desktop.
Simple java web-sockets demo
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This is java websockets exapmle: | |
Files: | |
- 01_GameServer.java - start server with Jetty Server inside | |
- 02_GameSocketServlet.java - mapping to ws://127.0.0.1:8210/gamestate url | |
- 03_GameSocket.java - handler of websocket connections | |
- 04_SocketPlayer.java - link between the server and the specific client | |
- 05_GameApp.java - the game engine | |
- 06_GameMap.java - map model | |
- 07_GameSession.java - game session with connected players | |
- 08_PlayerGameState.java - player's state in a session | |
- 11_game.js - client app |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.server; | |
import static och.util.Util.*; //just util class - nothing special | |
import game.app.GameApp; | |
import java.util.List; | |
import javax.servlet.annotation.WebServlet; | |
import org.apache.commons.logging.Log; | |
import org.eclipse.jetty.server.Handler; | |
import org.eclipse.jetty.server.Server; | |
import org.eclipse.jetty.servlet.ServletHandler; | |
public class GameServer { | |
static Log log = getLog(GameServer.class); | |
private static GameApp game; | |
public static GameApp app(){ | |
return game; | |
} | |
public static void main(String[] args) throws Exception { | |
log.info("start server..."); | |
int port = args.length > 0 ? tryParseInt(args[0], 8210) : 8210; | |
int period = args.length > 1 ? tryParseInt(args[1], 80) : 80; | |
String mapsPath = args.length > 2? args[2] : "./extra/websocket-testgame/maps"; | |
game = new GameApp(period); | |
List<String> loadedIds = game.loadMaps(mapsPath); | |
log.info("loaded maps: "+loadedIds.size()); | |
if(loadedIds.size() > 0) game.createGameSessionsWithMap(loadedIds.get(0)); | |
Server server = new Server(port); | |
server.setStopAtShutdown(true); | |
server.setHandler(createServlets()); | |
server.start(); | |
log.info("started"); | |
server.join(); | |
} | |
private static Handler createServlets() { | |
ServletHandler sh = new ServletHandler(); | |
addServletWithMapping(sh, GameSocketServlet.class); | |
return sh; | |
} | |
@SuppressWarnings({ "unchecked", "rawtypes" }) | |
public static void addServletWithMapping(ServletHandler sh, Class type){ | |
WebServlet annotation = (WebServlet)type.getAnnotation(WebServlet.class); | |
String[] paths = annotation.value(); | |
for (String path : paths) { | |
log.info("map path: "+path); | |
sh.addServletWithMapping(type, path); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.server; | |
import javax.servlet.annotation.WebServlet; | |
import org.eclipse.jetty.websocket.api.WebSocketPolicy; | |
import org.eclipse.jetty.websocket.servlet.WebSocketServlet; | |
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory; | |
@SuppressWarnings("serial") | |
@WebServlet(value="/gamestate") | |
public class GameSocketServlet extends WebSocketServlet { | |
@Override | |
public void configure(WebSocketServletFactory factory) { | |
WebSocketPolicy policy = factory.getPolicy(); | |
policy.setIdleTimeout(10000); | |
factory.register(GameSocket.class); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.server; | |
import static game.api.model.ClientMsg.*; | |
import static och.util.Util.*; | |
import game.api.model.ClientMsg; | |
import game.app.GameApp; | |
import org.apache.commons.logging.Log; | |
import org.eclipse.jetty.websocket.api.Session; | |
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose; | |
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect; | |
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage; | |
import org.eclipse.jetty.websocket.api.annotations.WebSocket; | |
@WebSocket(maxMessageSize = 64 * 1024) | |
public class GameSocket { | |
Log log = getLog(getClass()); | |
GameApp app; | |
public GameSocket() { | |
app = GameServer.app(); | |
} | |
@OnWebSocketConnect | |
public void onConnect(Session session) { | |
try { | |
String clientId = getClientSessionId(session); | |
app.addPlayerToGameSession(new SocketPlayer(clientId, session)); | |
} catch (Throwable t) { | |
log.error("error in onConnect", t); | |
} | |
} | |
@OnWebSocketMessage | |
public void onMessage(Session session, String msg) { | |
try { | |
String playerId = getClientSessionId(session); | |
if(isType(msg, READY_TO_START)){ | |
app.putPlayerToMap(playerId); | |
return; | |
} | |
if(isType(msg, KEY_DOWN)){ | |
app.onKeyDown(playerId, getMsgVal(msg, KEY_DOWN)); | |
return; | |
} | |
if(isType(msg, KEY_UP)){ | |
app.onKeyUp(playerId, getMsgVal(msg, KEY_UP)); | |
return; | |
} | |
} catch (Throwable t) { | |
log.error("error in onMessage", t); | |
} | |
} | |
@OnWebSocketClose | |
public void onClose(Session session, int statusCode, String reason) { | |
try { | |
String playerId = getClientSessionId(session); | |
app.removePlayerFromMap(playerId); | |
} catch (Throwable t) { | |
log.error("error in onClose", t); | |
} | |
} | |
public static String getClientSessionId(Session session) { | |
String out = session.getRemoteAddress().toString()+"@"+session.hashCode(); | |
return out; | |
} | |
public static boolean isType(String msg, ClientMsg type){ | |
return msg != null && msg.startsWith(type.name()); | |
} | |
public static String getMsgVal(String msg, ClientMsg type){ | |
return msg.substring(type.name().length()); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.server; | |
import static och.util.Util.*; | |
import game.api.model.ServerMsg; | |
import game.api.model.game.AddedToGameResp; | |
import game.api.model.game.GameMap; | |
import game.app.Player; | |
import org.apache.commons.logging.Log; | |
import org.eclipse.jetty.websocket.api.Session; | |
public class SocketPlayer implements Player { | |
static Log log = getLog(SocketPlayer.class); | |
String id; | |
Session session; | |
public SocketPlayer(String id, Session session) { | |
this.id = id; | |
this.session = session; | |
} | |
@Override | |
public String getId() { | |
return id; | |
} | |
@Override | |
public void onAddedToGameSession(GameMap map, int playerId) { | |
try { | |
if( ! session.isOpen()) return; | |
session.getRemote().sendStringByFuture(ServerMsg.IN_GAME+toJson(new AddedToGameResp(map, playerId))); | |
}catch(Throwable t){ | |
logError("can't sendCurMapTemplate", t); | |
} | |
} | |
@Override | |
public void onStateUpdated(String state) { | |
try { | |
if( ! session.isOpen()) return; | |
session.getRemote().sendStringByFuture(ServerMsg.STATE+state); | |
}catch(Throwable t){ | |
logError("can't sendCurMapTemplate", t); | |
} | |
} | |
private void logError(String msg, Throwable t){ | |
log.error(msg+": id="+id+", error="+t); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.app; | |
import static och.util.StringUtil.*; | |
import static och.util.Util.*; | |
import game.api.model.game.GameMap; | |
import game.app.model.GameSession; | |
import java.io.File; | |
import java.util.ArrayList; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.concurrent.ScheduledExecutorService; | |
import java.util.concurrent.TimeUnit; | |
import java.util.concurrent.locks.Lock; | |
import java.util.concurrent.locks.ReadWriteLock; | |
import java.util.concurrent.locks.ReentrantReadWriteLock; | |
import och.util.FileUtil; | |
import och.util.concurrent.ExecutorsUtil; | |
import org.apache.commons.logging.Log; | |
public class GameApp { | |
private static Log log = getLog(GameApp.class); | |
private ReadWriteLock rw = new ReentrantReadWriteLock(); | |
private Lock read = rw.readLock(); | |
private Lock write = rw.writeLock(); | |
private long period; | |
private ScheduledExecutorService pool; | |
//model | |
private Map<String, GameMap> allMapsTemplates = new HashMap<>(); | |
private Map<String, Player> allPlayers = new HashMap<>(); | |
private List<GameSession> gameSessions = new ArrayList<>(); | |
private Map<String, Integer> playerSessionIndex = new HashMap<>(); | |
public GameApp(long period) { | |
this.period = period; | |
reinitPool(); | |
} | |
public void reinitPool() { | |
if(pool != null) { | |
try { | |
pool.shutdownNow(); | |
}catch(Throwable t){ | |
log.error("can't shutdownNow pool", t); | |
} | |
} | |
pool = ExecutorsUtil.newScheduledThreadPool("update-game-sessions", Runtime.getRuntime().availableProcessors()); | |
} | |
public List<String> loadMaps(String dirPath) { | |
List<GameMap> loaded = new ArrayList<>(); | |
List<String> ids = new ArrayList<>(); | |
File dir = new File(dirPath); | |
File[] files = dir.listFiles(); | |
if( ! isEmpty(files)){ | |
for (File file : files) { | |
if( ! file.isFile()) continue; | |
GameMap map = null; | |
String name = file.getName(); | |
if(name.endsWith(".csv")){ | |
map = parseMapFromCsv(name, file); | |
} | |
if(map == null) continue; | |
loaded.add(map); | |
} | |
} | |
write.lock(); | |
try { | |
for (GameMap map : loaded) { | |
allMapsTemplates.put(map.id, map); | |
ids.add(map.id); | |
} | |
}finally { | |
write.unlock(); | |
} | |
return ids; | |
} | |
public void createGameSessionsWithMap(String mapId) { | |
write.lock(); | |
try { | |
GameMap map = allMapsTemplates.get(mapId); | |
if(map == null) return; | |
//TODO close old sessions | |
gameSessions.clear(); | |
reinitPool(); | |
//create new sessions | |
GameSession gameSession = new GameSession(map); | |
gameSessions.add(gameSession); | |
//start to update state | |
pool.scheduleWithFixedDelay(()->{ | |
gameSession.nextState(); | |
}, period, period, TimeUnit.MILLISECONDS); | |
}finally { | |
write.unlock(); | |
} | |
} | |
public void addPlayerToGameSession(Player p){ | |
write.lock(); | |
try { | |
if(gameSessions.isEmpty()) return; | |
String playerId = p.getId(); | |
if(playerSessionIndex.containsKey(playerId)) throw new IllegalArgumentException("player already exists"); | |
if( ! allPlayers.containsKey(playerId)) allPlayers.put(playerId, p); | |
int sessionIndex = 0; | |
GameSession gameSession = gameSessions.get(sessionIndex); | |
gameSession.addPlayer(p); | |
playerSessionIndex.put(playerId, sessionIndex); | |
}finally { | |
write.unlock(); | |
} | |
} | |
public void putPlayerToMap(String playerId) { | |
GameSession gameSession = getGameSession(playerId); | |
if(gameSession == null) return; | |
gameSession.putPlayerToMap(playerId); | |
} | |
public void onKeyDown(String playerId, String keyVal) { | |
GameSession gameSession = getGameSession(playerId); | |
if(gameSession == null) return; | |
gameSession.onKeyDown(playerId, keyVal); | |
} | |
public void onKeyUp(String playerId, String keyVal) { | |
GameSession gameSession = getGameSession(playerId); | |
if(gameSession == null) return; | |
gameSession.onKeyUp(playerId, keyVal); | |
} | |
public void removePlayerFromMap(String playerId) { | |
GameSession gameSession = getGameSession(playerId); | |
if(gameSession == null) return; | |
gameSession.removePlayerFromMap(playerId); | |
write.lock(); | |
try { | |
playerSessionIndex.remove(playerId); | |
allPlayers.remove(playerId); | |
}finally { | |
write.unlock(); | |
} | |
} | |
private GameSession getGameSession(String playerId){ | |
read.lock(); | |
try { | |
Integer sessionIndex = playerSessionIndex.get(playerId); | |
if(sessionIndex == null) return null; | |
if(sessionIndex >= gameSessions.size()) return null; | |
return gameSessions.get(sessionIndex); | |
}finally { | |
read.unlock(); | |
} | |
} | |
public static GameMap parseMapFromCsv(String id, File file) { | |
try { | |
List<List<Integer>> matrix = new ArrayList<>(); | |
String str = FileUtil.readFileUTF8(file); | |
List<String> lines = strToList(str, "\n"); | |
for (String line : lines) { | |
if( ! hasText(line) || line.startsWith("-1")) break; | |
String[] vals = line.trim().split(";"); | |
List<String> cells = list(vals); | |
List<Integer> mLine = convert(cells, (s)->tryParseInt(s, 0)); | |
matrix.add(mLine); | |
} | |
return new GameMap(id, matrix); | |
}catch(Throwable t){ | |
log.error("can't parseMapFromCsv", t); | |
return null; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.api.model.game; | |
import static game.api.model.Const.*; | |
import static game.api.model.game.CellType.*; | |
import static och.util.Util.*; | |
import java.util.Arrays; | |
import java.util.List; | |
public class GameMap { | |
public final String id; | |
private final byte[][] world; | |
public GameMap(String id, List<List<Integer>> initWorld){ | |
if(isEmpty(initWorld)) throw new IllegalArgumentException("initWorld is empty"); | |
int height = initWorld.size(); | |
int width = initWorld.get(0).size(); | |
if(width == 0) throw new IllegalArgumentException("first line in initWorld is empty"); | |
this.id = id; | |
this.world = new byte[height][width]; | |
//fill world | |
for (int i = 0; i < height; i++) { | |
List<Integer> line = initWorld.get(i); | |
int minWidth = Math.min(width, line.size()); | |
for (int j = 0; j < minWidth; j++) { | |
Integer cellType = line.get(j); | |
if(WALL.code == cellType) world[i][j] = WALL.code; | |
else world[i][j] = EMPTY.code; | |
} | |
} | |
} | |
public int height(){ | |
return world != null? world.length : 0; | |
} | |
public int width(){ | |
if(world == null)return 0; | |
return world.length > 0? world[0].length : 0; | |
} | |
public byte[] line(int lineIndex){ | |
if(lineIndex < 0 || lineIndex >= world.length) return null; | |
return world[lineIndex]; | |
} | |
public CellType getCellType(int lineIndex, int columnIndex){ | |
byte[] line = line(lineIndex); | |
if(line == null) return null; | |
if(columnIndex < 0 || columnIndex >= line.length) return null; | |
int type = line[columnIndex]; | |
return tryGetEnumByCode(type, CellType.class, null); | |
} | |
public boolean isBlocked(int x, int y) { | |
int lineIndex = y / cellWidth; | |
int columnIndex = x / cellWidth; | |
CellType cellType = getCellType(lineIndex, columnIndex); | |
if(cellType == null) return false; | |
return cellType != CellType.EMPTY; | |
} | |
@Override | |
public String toString() { | |
return "GameMap [id=" + id + ", world=" + Arrays.deepToString(world) + "]"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.app.model; | |
import static game.api.model.Const.*; | |
import game.api.model.game.GameMap; | |
import game.app.Player; | |
import game.app.model.PlayerGameState.Status; | |
import java.awt.Point; | |
import java.util.ArrayList; | |
import java.util.List; | |
import java.util.Random; | |
public class GameSession { | |
private static Random r = new Random(); | |
private GameMap map; | |
private List<PlayerGameState> playersState = new ArrayList<>(); | |
private int height; | |
private int nextPlayerId; | |
public GameSession(GameMap map) { | |
this.map = map; | |
height = map.height(); | |
} | |
public synchronized void addPlayer(Player p) { | |
int id = nextPlayerId++; | |
PlayerGameState state = new PlayerGameState(id, p); | |
playersState.add(state); | |
p.onAddedToGameSession(map, id); | |
} | |
public synchronized void putPlayerToMap(String playerId) { | |
PlayerGameState state = findState(playerId); | |
if(state == null) return; | |
if(state.status != Status.PREPARE_TO_GAME) return; | |
//add to map | |
Point p = findInsertPoint(); | |
state.status = Status.ON_MAP; | |
state.setPoint(p); | |
} | |
public synchronized void nextState(){ | |
int size = playersState.size(); | |
if(size == 0) return; | |
updateAllStates(); | |
StringBuilder sb = new StringBuilder(); | |
PlayerGameState state; | |
for (int i = 0; i < size; i++) { | |
state = playersState.get(i); | |
sb.append(state.id).append(';'); | |
sb.append(state.status.code).append(';'); | |
sb.append(state.x()).append(';'); | |
sb.append(state.y()).append(';'); | |
sb.append('\n'); | |
} | |
String sessionState = sb.toString(); | |
for (int i = 0; i < size; i++) { | |
playersState.get(i).p.onStateUpdated(sessionState); | |
} | |
} | |
public synchronized void onKeyDown(String playerId, String keyVal) { | |
PlayerGameState state = findActiveState(playerId); | |
if(state == null) return; | |
int velocity = 10; | |
if(keyUp.equals(keyVal)){ | |
state.velocityY = -velocity; | |
} | |
else if(keyDown.equals(keyVal)){ | |
state.velocityY = velocity; | |
} | |
else if(keyLeft.equals(keyVal)){ | |
state.velocityX = -velocity; | |
} | |
else if(keyRight.equals(keyVal)){ | |
state.velocityX = velocity; | |
} | |
} | |
public synchronized void onKeyUp(String playerId, String keyVal) { | |
PlayerGameState state = findActiveState(playerId); | |
if(state == null) return; | |
if(keyUp.equals(keyVal)){ | |
state.velocityY = 0; | |
} | |
else if(keyDown.equals(keyVal)){ | |
state.velocityY = 0; | |
} | |
else if(keyLeft.equals(keyVal)){ | |
state.velocityX = 0; | |
} | |
else if(keyRight.equals(keyVal)){ | |
state.velocityX = 0; | |
} | |
} | |
public synchronized void removePlayerFromMap(String playerId) { | |
int size = playersState.size(); | |
for (int i = 0; i < size; i++) { | |
if(playersState.get(i).p.getId().equals(playerId)){ | |
playersState.remove(i); | |
break; | |
} | |
} | |
} | |
private void updateAllStates() { | |
double timeDelta = 1d; | |
int size = playersState.size(); | |
int oldX; | |
int oldY; | |
PlayerGameState state; | |
for (int i = 0; i < size; i++) { | |
state = playersState.get(i); | |
oldX = state.x(); | |
oldY = state.y(); | |
state.updatePoint(timeDelta); | |
if(isBlocked(state.x(), state.y(), i)){ | |
state.setPoint(oldX, oldY); | |
} | |
} | |
} | |
private Point findInsertPoint() { | |
int tryCount = 10; | |
while(tryCount > 0){ | |
int yLine = r.nextInt(height); | |
int x = findInsertX(yLine); | |
if(x > -1){ | |
int y = yLine * cellWidth; | |
return new Point(x, y); | |
} | |
tryCount--; | |
} | |
throw new IllegalStateException("can't findInsertPoint: "+this); | |
} | |
private int findInsertX(int lineIndex) { | |
int y = lineIndex * cellWidth; | |
int x; | |
byte[] line = map.line(lineIndex); | |
int length = line.length; | |
int start = r.nextInt(length); | |
for (int i = start; i < length; i++) { | |
x = i * cellWidth; | |
if( ! isBlocked(x, y)){ | |
return x; | |
} | |
} | |
for (int i = start-1; i > -1; i--) { | |
x = i * cellWidth; | |
if( ! isBlocked(x, y)){ | |
return x; | |
} | |
} | |
return -1; | |
} | |
private boolean isBlocked(int x, int y) { | |
return isBlocked(x, y, -1); | |
} | |
private boolean isBlocked(int x, int y, int skipStateIndex) { | |
if(map.isBlocked(x, y)) return true; | |
int size = playersState.size(); | |
for (int i = 0; i < size; i++) { | |
if(i == skipStateIndex) continue; | |
if(playersState.get(i).isBlocked(x, y)) return true; | |
} | |
return false; | |
} | |
private PlayerGameState findActiveState(String playerId) { | |
PlayerGameState state = findState(playerId); | |
if(state == null) return null; | |
if(state.status != Status.ON_MAP) return null; | |
return state; | |
} | |
private PlayerGameState findState(String playerId) { | |
for (PlayerGameState state : playersState) { | |
if(state.p.getId().equals(playerId)) return state; | |
} | |
return null; | |
} | |
@Override | |
public synchronized String toString() { | |
return "GameSession [map=" + map + ", playersState=" + playersState | |
+ "]"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.app.model; | |
import static game.api.model.Const.*; | |
import game.app.Player; | |
import java.awt.Point; | |
import och.util.model.HasIntCode; | |
public class PlayerGameState { | |
public static enum Status implements HasIntCode { | |
PREPARE_TO_GAME(0), | |
ON_MAP(1) | |
; | |
public final int code; | |
private Status(int code) { | |
this.code = code; | |
} | |
@Override | |
public int getCode() { | |
return code; | |
} | |
} | |
public final int id; | |
public final Player p; | |
public Status status; | |
private double x; | |
private double y; | |
private int width = cellWidth; | |
private int height = cellWidth; | |
public int velocityX; | |
public int velocityY; | |
public PlayerGameState(int id, Player p) { | |
this.id = id; | |
this.p = p; | |
status = Status.PREPARE_TO_GAME; | |
} | |
public int x(){ | |
return (int)x; | |
} | |
public int y(){ | |
return (int)y; | |
} | |
public void setPoint(Point p){ | |
setPoint(p.x, p.y); | |
} | |
public void setPoint(int x, int y){ | |
this.x = x; | |
this.y = y; | |
} | |
public void updatePoint(double timeDelta) { | |
this.x = x + timeDelta * velocityX; | |
this.y = y + timeDelta * velocityY; | |
} | |
public boolean isBlocked(int x, int y) { | |
int thixX = x(); | |
int thisY = y(); | |
if(x >= thixX && x <= thixX + width){ | |
if(y >= thisY && y <= thisY + height){ | |
return true; | |
} | |
} | |
return false; | |
} | |
@Override | |
public String toString() { | |
return "[p=" + p + ", status=" + status + ", x=" + x | |
+ ", y=" + y + "]"; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package game.app; | |
import game.api.model.game.GameMap; | |
public interface Player { | |
String getId(); | |
void onAddedToGameSession(GameMap map, int playerId); | |
void onStateUpdated(String allStates); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE> | |
<html> | |
<head> | |
<meta charset='utf-8'/> | |
<link rel="stylesheet" href="css/app.css"> | |
<script type="text/javascript" src="js/jquery-1.9.1.min.js"></script> | |
<script type="text/javascript" src="js/lang.js"></script> | |
<script type="text/javascript" src="js/util.js"></script> | |
<script type="text/javascript" src="js/key-util.js"></script> | |
<script type="text/javascript" src="js/game.js"></script> | |
</head> | |
<body> | |
<h3> | |
Players online: <span id="playersCount"></span> | |
</h3> | |
<div id="root" class="root"> | |
</div> | |
<textarea id="log" style="width: 600px; height: 200px;"> | |
</textarea> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$(function(){ | |
new App().init(); | |
}); | |
var AppState = { | |
OFFLINE:"OFFLINE", | |
IN_GAME:"IN_GAME" | |
}; | |
var ServerMsg = { | |
IN_GAME:"IN_GAME", | |
STATE:"STATE" | |
}; | |
var ClientMsg = { | |
READY_TO_START:"READY_TO_START", | |
KEY_DOWN:"KEY_DOWN", | |
KEY_UP:"KEY_UP" | |
}; | |
var KeyCodes = { | |
38: "up", | |
40: "dwn", | |
37: "lft", | |
39: "rht" | |
} | |
function App(){ | |
var state = AppState.OFFLINE; | |
var logElem = $("#log"); | |
logElem.text(""); | |
var socket; | |
var connected = false; | |
var root = $("#root"); | |
var countElem = $("#playersCount"); | |
var playerId; | |
var playersElems = {}; | |
var keysdown = {}; | |
this.init = function(){ | |
if( ! window.WebSocket) { | |
alert("Web sockets not supported by this browser!"); | |
return; | |
} | |
socket = new WebSocket("ws://127.0.0.1:8210/gamestate"); | |
socket.onopen = function () { | |
log("Connection opened"); | |
connected = true; | |
}; | |
socket.onclose = function () { | |
log("Connection closed"); | |
connected = false; | |
}; | |
socket.onmessage = function (event) { | |
try { | |
var msg = event.data; | |
if(Util.isEmptyString(msg)) return; | |
if(msg.startsWith(ServerMsg.IN_GAME)){ | |
onInGame(getJsonMsg(ServerMsg.IN_GAME, msg)); | |
return; | |
} | |
if(msg.startsWith(ServerMsg.STATE)){ | |
onState(getMsg(ServerMsg.STATE, msg)); | |
return; | |
} | |
log("Unknown server msg: "+msg); | |
}catch(e){ | |
log("Error in server message hangler: "+Util.exceptionToString(e)); | |
} | |
}; | |
//key press | |
$(document).keydown(function(e){ | |
return onKeyDown(e); | |
}); | |
$(document).keyup(function(e){ | |
return onKeyUp(e); | |
}); | |
}; | |
function onInGame(data){ | |
if(state != AppState.OFFLINE) return; | |
showMap(data.map); | |
playerId = data.playerId; | |
state = AppState.IN_GAME; | |
socket.send(ClientMsg.READY_TO_START); | |
} | |
function onState(newState){ | |
if(state != AppState.IN_GAME) return; | |
var usersStates = newState.split("\n"); | |
if(Util.isEmptyArray(usersStates)) return; | |
var userState; | |
var x; | |
var y; | |
var id; | |
var status; | |
var elem; | |
var activePlayers = 0; | |
for (var i = 0; i < usersStates.length; i++) { | |
userState = usersStates[i].split(";"); | |
if(userState.length < 4) continue; | |
id = userState[0]; | |
status = userState[1]; | |
x = userState[2]; | |
y = userState[3]; | |
elem = getUserElem(id); | |
if(status == 0) { | |
elem.hide(); | |
continue; | |
} | |
activePlayers++; | |
elem.show(); | |
elem.css("left", x); | |
elem.css("top", y); | |
} | |
countElem.text(activePlayers); | |
} | |
function onKeyDown(e){ | |
if( ! connected) return true; | |
var code = KeyCodes[e.keyCode]; | |
if( ! code) return true; | |
if(keysdown[code]) return false; | |
keysdown[code] = true; | |
try { | |
socket.send(ClientMsg.KEY_DOWN+code); | |
return false; | |
}catch(e){ | |
log("can't onKeyDown: "+Util.exceptionToString(e)); | |
} | |
return true; | |
} | |
function onKeyUp(e){ | |
if( ! connected) return true; | |
var code = KeyCodes[e.keyCode]; | |
if( ! code) return true; | |
if( ! keysdown[code]) return false; | |
keysdown[code] = false; | |
try { | |
socket.send(ClientMsg.KEY_UP+code); | |
return false; | |
}catch(e){ | |
log("can't onKeyUp: "+Util.exceptionToString(e)); | |
} | |
return true; | |
} | |
function showMap(map){ | |
var matrix = map.world; | |
var table = $("<table></table>"); | |
for (var i = 0; i < matrix.length; i++) { | |
var tr = $("<tr></tr>"); | |
var line = matrix[i]; | |
for (var j = 0; j < line.length; j++) { | |
var td = $("<td></td>"); | |
var type = line[j]; | |
td.addClass("type-"+type); | |
tr.append(td); | |
} | |
table.append(tr); | |
} | |
root.append(table); | |
} | |
function getUserElem(id){ | |
var elem = playersElems[id]; | |
if(elem) return elem; | |
elem = $("<div></div>"); | |
elem.addClass("player"); | |
elem.text(id); | |
if(playerId == id) elem.addClass("curPlayer"); | |
root.append(elem); | |
playersElems[id] = elem; | |
return elem; | |
} | |
function getJsonMsg(msgType, msg){ | |
return JSON.parse(getMsg(msgType, msg)); | |
} | |
function getMsg(msgType, msg){ | |
return msg.substr(msgType.length); | |
} | |
function log(val){ | |
logElem.text(logElem.text()+"\n***\n"+val); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment