I wanted to do something more interesting than a chat room using Signalr, so in the long standing tradition of going way over the top and biting off more than I can chew, I decided I would build a multiplayer shooter game using Signalr. Over the long Thanksgiving holiday, much to my wife's chagrin, I stole every free moment to build this demo. I gotta say I'm pretty happy with the results.
You can check out the latest version of the code on GitHub: github.com/sabotuer99/BoxBlaster.
The Idea
I didn't want to do anything super fancy (my goal was to finish it over the four day weekend). So I figured something with boxes would be pretty straight forward. After being prompted for your name, you use the keyboard to control the movement of your box, and the mouse to aim and shoot. Black boxes on the playing field can be moved around by "pushing" them with your box. Since the point of the demo is supposed to be Signalr (and not game building), I'll describe how the game part works pretty briefly:
- JavaScript objects are used to represent the game objects in memory. Arrays of Box, Wall, PewPew (the little bullets), and Explosion objects are used to track the state of all the game objects.
- The playfield is an HTML5 canvas element. Rendering is performed by writing the game objects (from their respective arrays) to a temporary canvas, then "flipping" the temp canvas onto the display canvas (this prevents flickering). This render action is done every 40 milliseconds using setInterval (so around 25fps)
- Movement of the Player and all the PewPew objects is controled by a seperate timer. New x and y coodinates are calculated every 40 milliseconds (seperating it from the rendering seemed to help performance)
- Sounds have their volume adjusted based on the distance of their source from the player. So an explosion right next to the player is full volume, while one across the playfield is very faint.
- The Menu on the right side controls some basic functionality. Master volume is controlled by a slider, walls can be added and removed from the playfield, the Player can change the color of their box, and the aim line can be enabled and disabled.
- Collision detection uses a bit of geometry to detect intersecting line segments. Each object calculates it's own line segments with a built in method.
- All of the calculation is done on the client side. When a player respawns or moves, collision detection is performed. Adding walls to the field also uses collision detection to avoid spawning on top of players or other boxes.
Integrating Signalr
Once the Signalr NuGet package is installed and the OWIN startup class configured, it's time to start setting up the hub class. The hub will have to maintain a certain amount of state, since players that are just joining will need to know where existing boxes(players) and walls are located. Rather than trying to track the location of individual pewpews, they are tracked by each client, and when a new player joins all the pewpews on the field are cleared. Two classes are defined in the hub, Box and Wall, which hold just the state information that will be necessary to transmit back and forth. Two static lists (I know, cringe, but it's just a demo...) hold all the boxes and walls for a given game.
The connection process is easier to understand visually:
When a player first connects to the hub by calling $.connection.hub.start(), the OnConnect() method is called on the hub, which then proceeds to add all the boxes and walls onto the client by calling the client side methods .existingPlayerLoad() and .wallAdded(). It assigns the player a new id, and then calls the .pickNickname() method on the client, which prompts the user for a nickname and spawns the player on the field. The player returns this nickname and xy coordinates to the server, which then completes the joining process by adding the player object to the Boxes list and notifies all of the players of the new player. When the player is notified that they've joined, their controls and menu are enabled, and they can begin playing. Other clients will add the player's object to their local object lists and start recording actions.
Most of the other communication is pretty straight forward. Every time the player changes position, the new x and y is send to the server (after collision resolution). Firing signals to other users to spawn an equivalent projectile. Moving Walls around updates other clients. It is important that only changes effected by the player are communicated. If I move a wall, I notify everyone else. If someone else moves a wall, it shows up for me as a result of them telling me. If I change the color of my box, I tell everyone. If I remove or add a wall, I tell everyone. It's pretty simple.
Kills and deaths were an interesting problem. I could either report my own deaths, or my own kills. I ultimately opted to report my own kills. The one issue I had to work out was syncronizing time of death so that the person killed and everyone else all had the same respawn time (since the respawn runs on a timer). I had to make sure it was in UTC, but once the kinks were worked out it worked pretty well. I thought about doing something similar with the pewpews (if you moved aways from the window and then back, rendering got totally screwed up). This is ultimately why I opted to report kills from the player scoring the hit. A shooter is probably less likely to navigate away just as they are about to score a hit.
Notes on Azure
To really make this a cool demo, it had to be easily accessible, which meant hosting. I initially had it set up as a Standard website, but didn't want to burn through my monthly credit so I dropped it down to a Shared site. It works fine in Free mode too, except the 5 websocket connection limitation was a bit of a constraint. The interesting thing I found is that once you exhaust the Websockets connections, Signalr for the 6th client will gracefully fall back to ServerSentEvents (I also figured out that forgetting to turn on Websockets will have the same effect for everyone... oops). I must say, Websockets performed much better than SSE. I had four coworkers on at the same time and it got a bit choppy, but I had 8 windows open at once with Websockets and it was smooth as silk.
Deployment to Azure websites is trivial if you are using git, since all you have to do is push to the website repo as a remote. I did hit one weird issue when I tried to delete a file from the local repo and threw a deployment error on Azure when I pushed, but I just recreated the file and commited/pushed again and it was fine. I'm sure it could have been easily resolved, I just didn't feel like monkeying with it.
No comments:
Post a Comment