Created GameController, and generic controller. Created some methods in the client to send and receive data

This commit is contained in:
Martin Berg Alstad 2023-05-18 19:24:15 +02:00
parent ef85dec657
commit 19c87bae68
13 changed files with 237 additions and 72 deletions

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import {Counter} from "./pages/Counter"; import {Counter} from "./pages/counter";
import Home from "./pages/home"; import Home from "./pages/home";
const AppRoutes = [ const AppRoutes = [

View File

@ -7,12 +7,14 @@ interface IWebSocket {
export default class WebSocketService { export default class WebSocketService {
private ws?: WebSocket; private ws?: WebSocket;
private readonly _url: string;
private _onOpen?: VoidFunction; private _onOpen?: VoidFunction;
private _onReceive?: MessageEventFunction; private _onReceive?: MessageEventFunction;
private _onClose?: VoidFunction; private _onClose?: VoidFunction;
private _onError?: VoidFunction; private _onError?: VoidFunction;
constructor({onOpen, onReceive, onClose, onError}: IWebSocket) { constructor(url: string, {onOpen, onReceive, onClose, onError}: IWebSocket = {}) {
this._url = url;
this._onOpen = onOpen; this._onOpen = onOpen;
this._onReceive = onReceive; this._onReceive = onReceive;
this._onClose = onClose; this._onClose = onClose;
@ -20,22 +22,48 @@ export default class WebSocketService {
} }
public open(): void { public open(): void {
this.ws = new WebSocket("wss://localhost:3000/api/ws"); this.ws = new WebSocket(this._url);
}
public registerEvents(): void {
if (!this.ws) return;
if (this._onOpen) this.ws.onopen = this._onOpen; if (this._onOpen) this.ws.onopen = this._onOpen;
if (this._onReceive) this.ws.onmessage = this._onReceive; if (this._onReceive) this.ws.onmessage = this._onReceive;
if (this._onClose) this.ws.onclose = this._onClose; if (this._onClose) this.ws.onclose = this._onClose;
if (this._onError) this.ws.onerror = this._onError; if (this._onError) this.ws.onerror = this._onError;
} }
public send(data: string | ArrayBufferLike | Blob | ArrayBufferView) { public send(data: WebSocketData): void {
this.ws?.send(data); this.ws?.send(data);
} }
public close() { public async sendAndReceive<T>(data: WebSocketData): Promise<T> {
this.ws?.close(); if (!this.isOpen()) return Promise.reject("WebSocket is not open");
let result: T | undefined;
this.onReceive = (event: MessageEvent) => {
result = JSON.parse(event.data) as T;
};
this.send(data);
return new Promise<T>((resolve) => {
function f() {
if (result === undefined) {
setTimeout(f, 50);
return;
}
}
f();
return resolve(result!);
});
} }
public isOpen() { public async close(): Promise<void> {
return new Promise(() => this.ws?.close());
}
public isOpen(): boolean {
return this.ws?.readyState === WebSocket.OPEN; return this.ws?.readyState === WebSocket.OPEN;
} }

View File

@ -0,0 +1,23 @@
import React from "react";
interface AllDiceProps extends ComponentProps {
values: number[],
}
export const AllDice: Component<AllDiceProps> = ({className, values}) => {
return (
<>
{values?.map((value, index) => <Dice key={index} className={className}/>)}
</>
);
};
interface DiceProps extends ComponentProps {
value?: number,
}
export const Dice: Component<DiceProps> = ({className, value}) => {
return (
<div className={className}>{value?.toString()}</div>
);
};

View File

@ -1,18 +1,36 @@
import React from "react"; import React from "react";
import GameCanvas from "../components/gameCanvas"; import GameCanvas from "../components/gameCanvas";
import Game from "../game/game"; import Game from "../game/game";
import {AllDice} from "./dice";
export const GameComponent: Component = () => { export const GameComponent: Component = () => {
const [dice, setDice] = React.useState<number[]>([0, 0]);
React.useEffect(() => { React.useEffect(() => {
const game = new Game(); let game: Game = new Game();
const id = setInterval(game.gameLoop, 1000); game.connectToServer();
return () => clearInterval(id); function f() {
if (!game.isConnected()) {
setTimeout(f, 50);
return;
}
game.gameLoop(setDice);
}
f();
// TODO only call gameLoop after the previous one has finished
// const id = setInterval(() => game.gameLoop(), 5000);
// return () => clearInterval(id);
}, []); }, []);
React.useEffect(() => {
console.log(dice);
}, [dice]);
return ( return (
<div> <div>
<h1 className={"w-fit mx-auto"}>Pac-Man</h1> <h1 className={"w-fit mx-auto"}>Pac-Man</h1>
<AllDice values={dice}/>
<GameCanvas className={"mx-auto"}/> <GameCanvas className={"mx-auto"}/>
</div> </div>
); );

View File

@ -1,6 +1,11 @@
import WebSocketService from "../classes/WebSocketService";
export default class Game { export default class Game {
private wsService: WebSocketService;
constructor() { constructor() {
this.wsService = new WebSocketService("wss://localhost:3000/api/game");
// Connect to the server // Connect to the server
// Create players // Create players
@ -10,8 +15,12 @@ export default class Game {
// Roll to start // Roll to start
} }
public gameLoop(): void { public gameLoop(setDice: Setter<number[]>): void {
// Throw the dices // Throw the dices
this.rollDice().then((dices) => {
console.log(dices);
setDice(dices);
});
// Choose a dice and move pac-man or a ghost // Choose a dice and move pac-man or a ghost
@ -22,8 +31,13 @@ export default class Game {
// If not, next player // If not, next player
} }
private connectToServer(): void { public connectToServer(): void {
throw new Error("Not implemented"); this.wsService.open();
this.wsService.registerEvents();
}
public isConnected(): boolean {
return this.wsService.isOpen();
} }
private createPlayers(): void { private createPlayers(): void {
@ -38,8 +52,10 @@ export default class Game {
throw new Error("Not implemented"); throw new Error("Not implemented");
} }
private throwDices(): number[] { private async rollDice(): Promise<number[]> {
throw new Error("Not implemented"); let result: number[];
result = await this.wsService.sendAndReceive<number[]>("roll");
return result;
} }
private chooseDice(dices: number[]): number { private chooseDice(dices: number[]): number {

View File

@ -1,19 +1,19 @@
import React from "react"; import React from "react";
import WebSocketService from "../classes/WebSocketService"; import WebSocketService from "../classes/WebSocketService";
const ws = new WebSocketService({}); const ws = new WebSocketService("wss://localhost:3000/api/ws");
export const Counter: Component = () => { export const Counter: Component = () => {
const [currentCount, setCurrentCount] = React.useState(0); const [currentCount, setCurrentCount] = React.useState(0);
function incrementCounterAndSend() { async function incrementCounterAndSend() {
if (ws.isOpen()) { if (ws.isOpen()) {
ws.send((currentCount + 1).toString()); await ws.send((currentCount + 1).toString());
} }
} }
function receiveMessage(data: MessageEvent<any>) { function receiveMessage(data: MessageEvent<string>) {
const count = parseInt(data.data); const count = parseInt(data.data);
if (!isNaN(count)) if (!isNaN(count))
setCurrentCount(count); setCurrentCount(count);
@ -22,6 +22,7 @@ export const Counter: Component = () => {
React.useEffect(() => { React.useEffect(() => {
ws.onReceive = receiveMessage; ws.onReceive = receiveMessage;
ws.open(); ws.open();
ws.registerEvents();
return () => { return () => {
ws.close(); ws.close();
}; };

View File

@ -1,3 +1,5 @@
type MessageEventFunction = (data: MessageEvent<any>) => void; type MessageEventFunction = (data: MessageEvent<any>) => void;
type Setter<T> = React.Dispatch<React.SetStateAction<T>>; type Setter<T> = React.Dispatch<React.SetStateAction<T>>;
type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView;

View File

@ -0,0 +1,35 @@
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using pacMan.Game.Interfaces;
using pacMan.Game.Items;
using pacMan.Interfaces;
using pacMan.Utils;
namespace pacMan.Controllers;
[ApiController]
[Route("api/[controller]")]
public class GameController : GenericController
{
private readonly IDiceCup _diceCup;
public GameController(ILogger<GameController> logger, IWebSocketService wsService) : base(logger, wsService)
{
_diceCup = new DiceCup();
}
[HttpGet]
public override async Task Accept() => await base.Accept();
protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data)
{
var stringResult = data.GetString(data.Length);
Logger.Log(LogLevel.Information, "Received: {}", stringResult);
var rolls = _diceCup.Roll();
Logger.Log(LogLevel.Information, "Rolled {}", string.Join(", ", rolls));
return rolls.ToArraySegment();
}
}

View File

@ -0,0 +1,63 @@
using System.Net.WebSockets;
using Microsoft.AspNetCore.Mvc;
using pacMan.Interfaces;
namespace pacMan.Controllers;
public abstract class GenericController : ControllerBase
{
protected readonly ILogger<GenericController> Logger;
private readonly IWebSocketService _wsService;
private const int BufferSize = 1024 * 4;
protected GenericController(ILogger<GenericController> logger, IWebSocketService wsService)
{
Logger = logger;
_wsService = wsService;
Logger.Log(LogLevel.Debug, "WebSocket Controller created");
}
public virtual async Task Accept()
{
if (HttpContext.WebSockets.IsWebSocketRequest)
{
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
Logger.Log(LogLevel.Information, "WebSocket connection established to {}", HttpContext.Connection.Id);
_wsService.Add(webSocket);
await Echo(webSocket);
}
else
{
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
}
protected virtual async Task Echo(WebSocket webSocket)
{
try
{
var buffer = new byte[BufferSize];
WebSocketReceiveResult? result;
do
{
result = await _wsService.Receive(webSocket, buffer);
if (result.CloseStatus.HasValue) break;
var segment = Run(result, buffer);
await _wsService.SendToAll(segment);
} while (true);
await _wsService.Close(webSocket, result.CloseStatus.Value, result.CloseStatusDescription ?? "No reason");
}
catch (WebSocketException e)
{
Logger.Log(LogLevel.Error, "{}", e.Message);
}
_wsService.Remove(webSocket);
}
protected abstract ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data);
}

View File

@ -6,57 +6,18 @@ namespace pacMan.Controllers;
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/[controller]")]
public class WsController : ControllerBase public class WsController : GenericController
{ {
private readonly ILogger<WsController> _logger; public WsController(ILogger<WsController> logger, IWebSocketService wsService) : base(logger, wsService)
private readonly IWebSocketService _wsService;
private const int BufferSize = 1024 * 4;
public WsController(ILogger<WsController> logger, IWebSocketService wsService)
{ {
_logger = logger;
_wsService = wsService;
_logger.Log(LogLevel.Debug, "WebSocket Controller created");
} }
[HttpGet] [HttpGet]
public async Task Get() public override async Task Accept() => await base.Accept();
protected override ArraySegment<byte> Run(WebSocketReceiveResult result, byte[] data)
{ {
if (HttpContext.WebSockets.IsWebSocketRequest) var segment = new ArraySegment<byte>(data, 0, result.Count);
{ return segment;
using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
_logger.Log(LogLevel.Information, "WebSocket connection established to {}", HttpContext.Connection.Id);
_wsService.Add(webSocket);
await Echo(webSocket);
}
else
{
HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
}
}
private async Task Echo(WebSocket webSocket)
{
try
{
var buffer = new byte[BufferSize];
WebSocketReceiveResult? result;
do
{
result = await _wsService.Receive(webSocket, buffer);
if (result.CloseStatus.HasValue) break;
await _wsService.SendToAll(buffer, result.Count);
} while (true);
await _wsService.Close(webSocket, result.CloseStatus.Value, result.CloseStatusDescription ?? "No reason");
}
catch (WebSocketException e)
{
_logger.Log(LogLevel.Error, "{}", e.Message);
}
_wsService.Remove(webSocket);
} }
} }

View File

@ -8,8 +8,10 @@ public interface IWebSocketService
bool Remove(WebSocket webSocket); bool Remove(WebSocket webSocket);
Task Send(WebSocket webSocket, string message, int length); Task Send(WebSocket webSocket, string message, int length);
Task Send(WebSocket webSocket, byte[] message, int length); Task Send(WebSocket webSocket, byte[] message, int length);
Task Send(WebSocket webSocket, ArraySegment<byte> segment);
Task SendToAll(string message, int length); Task SendToAll(string message, int length);
Task SendToAll(byte[] message, int length); Task SendToAll(byte[] message, int length);
Task SendToAll(ArraySegment<byte> segment);
Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer); Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer);
Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string closeStatusDescription); Task Close(WebSocket webSocket, WebSocketCloseStatus closeStatus, string closeStatusDescription);
int CountConnected(); int CountConnected();

View File

@ -8,7 +8,7 @@ namespace pacMan.Services;
public class WebSocketService : IWebSocketService public class WebSocketService : IWebSocketService
{ {
private readonly ILogger<WebSocketService> _logger; private readonly ILogger<WebSocketService> _logger;
private readonly SynchronizedCollection<WebSocket> _webSockets = new(); private readonly SynchronizedCollection<WebSocket> _webSockets = new(); // TODO separate connections into groups
public WebSocketService(ILogger<WebSocketService> logger) public WebSocketService(ILogger<WebSocketService> logger)
{ {
@ -38,15 +38,18 @@ public class WebSocketService : IWebSocketService
public async Task Send(WebSocket webSocket, byte[] message, int length) public async Task Send(WebSocket webSocket, byte[] message, int length)
{ {
var msgSegment = new ArraySegment<byte>(message, 0, length); var msgSegment = new ArraySegment<byte>(message, 0, length);
await Send(webSocket, msgSegment);
}
public async Task Send(WebSocket webSocket, ArraySegment<byte> segment)
{
await webSocket.SendAsync( await webSocket.SendAsync(
msgSegment, segment,
WebSocketMessageType.Text, WebSocketMessageType.Text,
true, true,
CancellationToken.None); CancellationToken.None);
_logger.Log(LogLevel.Trace, _logger.Log(LogLevel.Trace, "Message sent to WebSocket");
"Message \"{}\" sent to WebSocket",
message.GetString(length));
} }
public async Task SendToAll(string message, int length) public async Task SendToAll(string message, int length)
@ -62,6 +65,11 @@ public class WebSocketService : IWebSocketService
_logger.Log(LogLevel.Debug, "Message sent to all WebSockets"); _logger.Log(LogLevel.Debug, "Message sent to all WebSockets");
} }
public async Task SendToAll(ArraySegment<byte> segment)
{
foreach (var ws in _webSockets) await Send(ws, segment);
}
public async Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer) public async Task<WebSocketReceiveResult> Receive(WebSocket webSocket, byte[] buffer)
{ {
var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None); var result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
@ -80,6 +88,6 @@ public class WebSocketService : IWebSocketService
CancellationToken.None); CancellationToken.None);
_logger.Log(LogLevel.Information, "WebSocket connection closed"); _logger.Log(LogLevel.Information, "WebSocket connection closed");
} }
public int CountConnected() => _webSockets.Count; public int CountConnected() => _webSockets.Count;
} }

View File

@ -1,4 +1,5 @@
using System.Text; using System.Text;
using System.Text.Json;
namespace pacMan.Utils; namespace pacMan.Utils;
@ -8,4 +9,11 @@ public static class Extensions
{ {
return Encoding.UTF8.GetString(bytes, 0, length); return Encoding.UTF8.GetString(bytes, 0, length);
} }
public static ArraySegment<byte> ToArraySegment(this object obj)
{
var json = JsonSerializer.Serialize(obj);
var bytes = Encoding.UTF8.GetBytes(json);
return new ArraySegment<byte>(bytes, 0, json.Length);
}
} }