As we add more games, we want to keep this well organized, sharing react components where it makes sense and keeping each game confined to its own Hub and its own Manager.
This project is set up to easily launch with VSCode. Mainly you should just need the following extensions installed:
- C#
- Debugger for Chrome
- ESLint
- Prettier
Launching via F5 will just launch Chrome and run both client and server.
This application is containerized, using a Dockerfile and hosted on DockerHub. Each merge/commit to the master kick off an action that will build and deploy the docker image to a Azure web application.
Since we're continuously deploying, All active development should be done on a branch and fully functional before we merge it to master.
The front end is written in React using Typescript. Currently there are no unit tests for react components, but that's something we can add in the future. When we add tests, each component's test should live alongside the component in its own folder.
My general approach to React components for this site has been to use React Hooks as much as possible. While I'm familiar with their use, occasionally they still don't fully make sense to me particularly when coupled with SignalR events and it's easier to just use a class in those cases. But I'd like to strive to use hooks whenever possible.
I usually keep my chrome developer console open while i'm working on the UI and fix all warnings that show up there.
| Folder | Description |
|---|---|
| /src/HousePartyGames/ClientApp | Front end code root |
| /src/HousePartyGames/ClientApp/src/Components | Folder for all React components |
| /src/HousePartyGames/ClientApp/src/Components/Common | Shared React components for all games |
| /src/HousePartyGames/ClientApp/src/Components/Common/VectorGraphics | Each SVG lives in its own tsx file here and cane be used with the SvgIcon component to render in your React component. |
| /src/HousePartyGames/ClientApp/src/Components/GameUI | Top level folder for all the UI for a game |
| Library | Purpose |
|---|---|
| Material UI | This is the core UI component library for the website for a uniform look. Open to changing to a different library in the future, but this one is pretty standard. Visit https://material-ui.com/ for examples |
| Axios | Though most of the game state communcation is managed with signalr, this library is used for making standard http requests to the back end service when needed. |
| SignalR | Used to maintain websocket connections between the browser and web service. This is used to keep all clients' in sync with the game state on the server. |
The web service is written in .NET Core and uses SignalR for web socket communication with each connected browser.
| Folder | Description |
|---|---|
| /src/HousePartyGames/Hubs | Each game should have its own hub in this folder. |
| /src/HousePartyGames/Managers | Each game should have its own Manager in this folder that fully manages its game state. threadsafe. |
The hub's responsibility is to maintain the collection of connected clients as well as communication to them. It should only be aware of game state in the sense of what to call in the manager.
Note that if a request comes from the client and a response can be returned right away, with SignalR you can call:
await Clients.Client("some-connection-id").SendAsync("some-method", someArgs);
But if the call is originating from another place in code, say from a Timer you need to make that call this way:
await _callbackContext.Clients.Client("some-connection-id").SendAsync("some-method", someArgs);
where _callbackContext is an IHubContext<YourHub> which is injected in the Startup class.
To map a hub to a specific endpoint that's done in the Startup.cs. Map your hub in this section:
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<ThatsItHub>("/thatsitgame");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
});
NOTE: make sure your hub doesn't conflict with a React Route! So if the path to my game in the browser was: housepartygames.com/chess, I would map my hub with something that isn't chess or that will conflict. So maybe append a string to it and call it: /chessgame.
It should return data to the hub or callback into a method on the hub when it is finished with a process and needs to send state to the connected clients. One important thing to remember with the manager is that each change to the state needs to be threadsafe. I've created a convenience wrapper to handle that. Use it like this:
await ActiveGames.WithLockedInstance(joinCode, async (GameInstance instance) =>
{
...
});
Currently all game state is stored in memory. If deploy the site all active games will get WIPED.
- Use persistent storage
There is a background service that runs to clean up games that have ended (or have been abandoned). This is implemented as a hosted service because this allows a user to refresh their browser and stay connected to the game.
The original implementation would check if a game had any connected clients (players) and dispose of the game instance if that was the last client to disconnect. This caused problems where joining a game and then refreshing your browser would disconnect and reconnect, but if you were the only player in the game, the game would be gone. Similarly if you just let your phone go to sleep you get disconnected and your game will get disposed.
Now, you can disconnect your game state will remain intact until some time threshold has been reached and then it will get disconnected. Each game's Manager should implement a method that handles the cleanup and then add the call to it in the cleanup service.