Online Multiplayer Word Game With Blazor and SignalR on .NetCore

Bora Kaşmer
14 min readAug 8, 2020

Today we will make a simple online multiplayer word game with Blazor. We will use SignalR for realtime communication between two players. All the words will keep in MongoDB. And we will support multi-platforms with .Net Core. “Current .Net Core version 3.1.301”

We do not stop playing because we grow old. We grow old because we stop playing.
— Benjamin Franklin

Let’s Create Blazor Server App

dotnet new blazorserver -o blazorWords

Add .Net Core SignalR.Client

dotnet add package Microsoft.AspNetCore.SignalR.Client

Add MongoDB Driver

dotnet add package MongoDB.Driver

Hot Reload Settings For Blazor

When I change the blazor codes, I don’t want to build and refresh the browser again and again. So I added these configurations. After all, when I change the codes, the browser will be refreshed automatically.

1-) _Host.cshtml: I addedonConnectionDown()function into development environment. When the connection is down, this function waits five seconds and reloads the page again.

<environment include="Development">
<script>
//For Hot Reload
//dotnet watch run debug
window.Blazor.defaultReconnectionHandler.onConnectionDown = function ()
{
setTimeout(function () {
location.reload();
}, 5000);
};
</script>
</environment>

2-) And Run .Net Core Blazor Application with Watch:

That’s all we must do to Hot Reload the Blazor application.

dotnet watch run debug

That’s all. We are all set. Now lets coding. We will write a real-time game name guess quiz.

Game Rules

1Every time one of the game will be selected from the MongoDB randomly. I use Robo 3T as a Visual Manager Tool for the MongoDB. I created the words Database and wordLibrary collection on Mongo, as seen below.

2All players must Login with a user name. Without both players not login, the game will not start. Only two players can play at the same time.

3After both player login, all letters of the selected games name will be shown as a box on the page. So we will hide the game name.

Life is a game. Money is how we keep score.
— Ted Turner

4 In the beginning, all the players have 100₺ money. When they click the box and show the letter of the word, they lose 10₺ money. If you don’t have any money, you can not click any box and not seen any letter of the game’s name.

5 If you write the answer correctly, you can earn 10₺ money for every not open box. And Total Win Match count increase one. In the end, the new word comes randomly for both players.

.Net Core Blazor Applicaton BlazorWord (Client Side):

Shared/NavMenu.razor: Addwordgame” link to the left menu on the Blazor application.

.
.
<li class="nav-item px-3">
<NavLink class="nav-link" href="wordgame">
<span class="oi oi-list-rich" aria-hidden="true"></span> WordGame
</NavLink>
</li>
</ul>

Pages/Words.razor(1): “wordgame” is the routing keyword of this page. And we added signalR, NavigationManager is used for connecting the signalR wordHub class, and IJRuntime libraries for trigger javascript codes and navigate hub manager.

@implements IDisposable is a critical library. If you remove this library from the page, when the client is logout, the signalR wordHub OnDisconnectedAsync() method is not triggered.

@page "/wordgame"
@using Microsoft.AspNetCore.SignalR.Client
@inject NavigationManager NavigationManager
@implements IDisposable
@inject IJSRuntime JSRuntime

Pages/Words.razor(2):

At the same time, only two players can play. isFullRoom boolean variable is used for checking the total number of players. If total player count is more then two, it is “true” else it is “false.” @globalScore integer variable is used for the player total winning games count.

Pages/Words.razor(2):

@if(!isFullRoom) {
<span style="display: flex; justify-content: flex-end">
<div><h3><b><font color="#46CB18">Total Win Match: @globalScore</font></b></h3></div>
</span>
}

You never win any games you don’t play.
— Mark Cuban

Pages/Words.razor(3):

“@if(!isLogin && !isFullRoom) {“ : If you are not login and total player count less then two, User Input and Login button will be seen.

If you are login and you are one of the two player, you will see your User name, Total Money, Answer Input, and Answer Button. Answer Input @bind=”answer” variable, and when we click the Answer button, we will call the Answer() method. @onclick=”Answer”

Pages/Words.razor(3):

Pages/Words.razor(4):

Other player’s Total Money(@_otherMoney) and User name(@_otherUser) are seen in this div. After the first player is log in, the second player sees this page when he or she comes.

Pages/Words.razor(4):

<div class="form-group">
@if(_otherUser!=null) {
<label><h2><b>Other User :</b> @_otherUser</h2></label> <br>
<label><h2><b>Other User's Money :</b> @_otherMoney ₺</h2> </label>
}
</div>

Pages/Words.razor(5)

  • After both players log in, we will get a random word
    @if(word!=null) {.” And this div will be shown as a box image list on the page.
  • Every box reference a letter of the game name.
    @for (int i=0;i<word.Length;i++)
  • We will create a dynamic id for every image and label.
    string imgId=”image-”+@i; string lblId=”label-”+@i;
  • We will put a hidden label for every letter of the word.
    <span style=”display: none;” id=”label-@i”>@word[i].ToString().ToUpper()</span>
  • If the letter is not space “if(word[i]!=’ ‘)”, we will show the box image. When we click it, we will call the Open() method. We will hide the box image and show the label of the letter.
    <img src=”/Images/block.png” id=”image-@i” asp-append-version=”true” width=”50px” style =”display: inline;” @onclick=”@(() => Open(i,imgId,lblId,false))”/>
  • For every space letter of the word, we will show an empty box image.
    <img src=”/Images/blockEmpty.png” id=”image-@i” asp-append-version=”true” width=”50px” style =”display: inline;”/>

Pages/Words.razor(5):

This part is the code base of the Blazor (Server Side)

Pages/Words.razor(6):

This part is the server-side code of the Blazor application. Our backend codes are written between “@code{” and “}” tags. We will declare all global variables on the top of the backend codes.

  • globalScore: Player total win count.
  • isFullRoom: For this game, the total player count must be two if the total count is two isFullRoom variable sets true else false.
  • ReferenceToLoginControl: I used this reference for focusing the login input when the page start.
  • hubConnection: We will use this object to connect to the signalR Hub class.
  • userName: This is our user name.
  • _otherUser: This is opponent’s username.
  • _otherMoney: This is opponent’s money.
  • word: This is the word, which both two players try to find the game’s name.
  • connectionID: This is the unique signalR Hub connectionID. It is different for every player. And it is changing for every refresh.
  • isLogin: Is a player enter his or her Username or not.
  • money: This is the total money of the player.
  • answer: This is the variable that the player guesses the game name.

We will connect WordHub signalR class with this code.

hubConnection = new HubConnectionBuilder()            .WithUrl(NavigationManager.ToAbsoluteUri("/wordhub"))            .Build();

If this is the first time coming player to hub class, we will send him or her to signalR Hub connectionID with null otherUser and otherMoney parameters. And if we do not call the “StateHasChanged()” method, the view can not rerender again with the new binding models.

JSRuntime.InvokeAsync<object>”: You can not call javascript function directly on Blazor. You need JRuntime library.

Pages/Words.razor(7)

SendUserInformation()” is used for when the new player comes, sending other connected player’s money and userName information to this new player. Total Connected player count must be one. For deliver the connected player’s info to the new player, we will call SendUserInformation() method of the SignalR Hub class.

“GetUserInformation()” is used for when the player connects the signalR hub class, his or her getting new connectionID from the hub server and getting other player’s UserName and money info. Total Connected player count must be one.

Pages/Words.razor(8)

ReceiveUser(): It is used for when the other client connects to Hub Server, we get his or her userName and Money information and show the page. Two of the players must login for trigger to this function.

“RemoveUser()”: Whichever developer comes out, this method is called. We remove other User information, and the word tried to be guessed from the screen.

ReceiveWord: When both players are login, we will select random word form the list and send it all players with this function. And also we will send username and money info.

hubConnection.On<string, string, int>("ReceiveWord", (_wordText, _userName, _money) =>
{
if (userName != _userName)
{
_otherUser = _userName;
_otherMoney = _money;
}
else
{
money = _money;
isLogin = true;
}
word = _wordText;
StateHasChanged();
});

Pages/Words.razor(9)

RefreshWord(): When one of the players call “Answer()” function and win the game, we will call “Refresh()” hub method. This method gets a random keyword and sends it to all clients by trigger “RefreshWord()” function.

  • In this function, we will generate a box image and a letter label for every letter of the word.
    for (int i = 0; i < word.Length; i++)
  • string imgId = “image-” + @i;” : This unique box of imageID.
  • await JSRuntime.InvokeVoidAsync(“applyStyleForElement”, new { id = imgId, attrib = “display”, value = “inline” });” : This is set image “display” attribute to “inline
  • await JSRuntime.InvokeVoidAsync(“applyStyleForElement”, new { id = lblId, attrib = “display”, value = “none” }, new { id = lblId, attrib = “font-size”, value = “65px” });” : This is set label “display” attribute to “none” and change “font-size” to “65px”.

We will talk about “function applyStyleForElement()” later in this article.

You can not directly set the attribute of HTML elements on Blazor. So you must use “JSRuntime.InvokeVoidAsync() “ method. For trigger the javascript function.

  • word = _wordText;answer = “”;”: Set the word to the _wordText variable.
  • StateHasChanged();” : You have to call this method, for re-render the html.
hubConnection.On<string>("RefreshWord", async (_wordText) =>
{
for (int i = 0; i < word.Length; i++)
{
if (word[i] != ' ')
{
string imgId = "image-" + @i;
string lblId = "label-" + @i;
await JSRuntime.InvokeVoidAsync("applyStyleForElement",
new { id = imgId, attrib = "display", value = "inline" });
await JSRuntime.InvokeVoidAsync("applyStyleForElement",
new { id = lblId, attrib = "display", value = "none" },
new { id = lblId, attrib = "font-size", value = "65px" });
}
}
word = _wordText;answer = "";
StateHasChanged();
});

Pages/Words.razor(10)

ReceiveOpen(): When one of the players click the word box image, we will deliver this event to the other player by using this function.

ComeLater(): If a new player connects to signalR, but total player’s count is two he or she can’t log in the game so we will not allow joining the game to this user by showing above picture.

Open() BoxImage:

  • if (isOtherOpen == false && money >= 10)” : If you open yourself and if you have money at least 10₺ you can open the clicked box image.
  • await JSRuntime.InvokeVoidAsync(“applyStyleForElement”, new { id = imgId, attrib = “display”, value = “none” });”: We will trigger “applyStyleForElement” function. And we will hide the selected box image.
  • await JSRuntime.InvokeVoidAsync(“applyStyleForElement”, new { id = lblId, attrib = “display”, value = “inline” }, new { id = lblId, attrib = “font-size”, value = “65px” });” : We will the show image of the letter by using the display selected hidden label.

We can not set the Html Element Property directly on Blazor. So we have to trigger out of the javascript function with “JSRuntime.InvokeVoidAsync

  • await hubConnection.SendAsync(“OpenClient”, counter, imgId, lblId, money);” : We will notify the other user to display the hidden letter of the label, hide the clicked box image and finally his or her latest amount of money.
  • If we are not user, who clicked one of the box images, all the scenario is the same except we will not notify the other user because we are the notified user for this case.

Send(): When we enter the username and click the Login button, we will call the Send() method. And Send method calls the LoginUser() method on HubClass.

Task Send() =>
hubConnection.SendAsync("LoginUser", userName, connectionID);

Pages/Words.razor(11)

Answer(): When one of the players, write the game name on the input field, we will check the accuracy. If it is correct, we will increase the player money and global score. And We will send the notification to the other player.

  • if (answer.ToUpper() == word.ToUpper())” : Check the accuracy of the answer.
  • globalScore++; for (int i = 0; i < word.Length; i++)” : Increase globalScore. Loop into every character of the word.
  • await JSRuntime.InvokeAsync<bool>(“getStyleForElement”, new { id = “label-” + @i, attrib = “display”});” : We check every display property of the label. If it is not “inline” and not empty, you earn 10₺. This mean, you didn’t open this character. You will earn 10₺ for every not opened letter of the word.
  • await JSRuntime.InvokeVoidAsync(“applyStyleForElement”, new { id = imgId, attrib = “display”, value = “none” });” : If the answer is correct, we will hide every box image. And we will show every label words.
  • await hubConnection.SendAsync(“sendAnswer”, userName, connectionID,money);” : We will send the winner username and latest money value to the other user.
  • await JSRuntime.InvokeAsync<object>(“alert”, “Winner :” + userName +”\n Total Reward :” +totalEarnMoney+”₺”);” : We will invoke the alert function to show the winner message to the screen.
  • await hubConnection.SendAsync(“Refresh”);” : We will call the Refresh() method of the hub server. And it will trigger “RefreshWord()” function of all connected clients to show the new word question.

This variable is used for to learn is the client connected or not.

public bool IsConnected =>hubConnection.State == HubConnectionState.Connected;

Pages/Words.razor(12)

IsRenderUI : This variable is used for the understanding of the page loading is finished or not.

OnAfterRender()” after the page is load; we will set this variable as a true. “GetConnectionId()” and “GetUserInformation()” is not work before page is loaded.

OnAfterRenderAsync() : After the page is loaded, we will focus pointer to the userName input field.
User: <input @bind=”userName” @ref=ReferenceToLoginControl/>

bool IsRenderUI = false;
protected override void OnAfterRender(bool firstRender)
{
IsRenderUI = true;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
await JSRuntime.InvokeVoidAsync("FocusScript.setFocus", ReferenceToLoginControl);
}

_Host.cshtml(JavaScript):

We declare all Global javascript function on _Host.cshtml file.

Blazor can not set HTML elements to attribute directly. It is only triggered exist javascript functions with “JSRuntime.InvokeVoidAsync()” method.

  • applyStyleForElement: It is used for finding HTML elements by its Id and set one ore two attributes with parameters value.
  • getStyleForElement: We will get display style of the HTML element. If it is “inline” we will return “true” otherwise we will return “false”.
  • FocusScript: It is used for focusing the selected HTML element.

WordHub.cs SignalR Hub Class WordHub

We will keep the clients, who connected to WordHub Socket in this dictionary List. Dictionary is Concurrent because we are working with “asynchronous“ methods. Concurrent list-objects lock other tasks when you get an item from in it.

static ConcurrentDictionary<string, string> clientList = new ConcurrentDictionary<string, string>();

IWordService is injected into WordHub class. It is used for getting all name of games from the MongoDB and fill the List of words.

private IWordService _service;
static List<Words> words;
public WordHub(IWordService service)
{
_service = service;
}

WordHub.cs/OnConnectedAsync:

When a client connects to WordHub signalR Socket, he or she gets new connectionID from the WordHub Socket. If she is the first player, she will get her own connectionID by triggered the “GetConnectionId()” method, if she is second, she will trigger the SendUserInformation() message with her conenctionID parameter to get the name and money information of the other player. If she is third, she can not log in the game, and she will notified by the “ComeLater()” function.

WordHub.cs/SendUserInformation:

SendUserInformation is used for delivering the connected other player’s info to the new coming player.

WordHub.cs/Refresh:

If one of the players give the correct answer, we call this method from the client to randomly get the next word from the game list. And we will send it to both players. The selected name is removed from the words list, and if the list is empty, we will again fill it from the MongoDb.

WordHub.cs/AddList:

This method is called from the LoginUser() method. If one of the clients connect to Hub class, we will add his or her username with the connectionID value to this static ConcurrentDictionary clientList. If both players join to Hub, we will select a new word randomly and send it to all clients by triggered “ReceiveWord()” function. If only one player joins the Hub class, we will send his or her username, connectionID and money information by triggered the “ReceiveUser()” function.

WordHub.cs/OpenClient:

When one of the clients click the box image and open the letter, we will notify the other player with this OpenClient() hub method. We will send index number, opened letter labelID, hidden box imageID and latest money of current player to other players.

WordHub.cs/sendAnswer:

When one of the clients answer the game name correctly, we will notify this event to the other player with this sendAnswer() hub method. We will send winner userName, money and connectionID to the other player

WordHub.cs/OnDisconnectedAsync:

When one of the clients close the hubConnect, this “OnDisconnectedAsync(,)” method will be triggered. We will get a connectionID of the disconnected client. We will remove it from the clientList. If we still have any online player, we will send this player to removeUser name and connectionID parameters by triggering the RemoveUser() client function.

Models/Words:

This is the data model that we keep in MongoDB. We add “ [BsonId]” , “[BsonRepresentation(BsonType.ObjectId)]”attributes to Id property. This means, MongoDB set this ID property as a Guide automatically.

Data/WordService:

This service is used to connect to MongoDB and get all Game Names (Words) from the “wordLibrary” Collection.

Conclusion:

In this article, we examined in-depth Blazor and Socket technologies. Of course, blazor is very new and need some improvements, but it makes things very short and practical. Working on backend and front-end on the same page is a lovely experience. There is much to do in Blazor about Javascript, hot reload and server-side flexibility. If you write a game, signalR socket is a very handy tool. It is used to communicate between two clients in a real-time. Every movement and client info like money, username, etc. sends to the opponent in a real-time. I used MongoDB for storing the words. I get all words into static Concurrent list-object. I get all words from MongoDB into Concurrent static list-object. Every time I got one of the words randomly and removed it from the list. When the list is empty, I fill the list again from the DB. So if the new words come to DB, I will get them at the end of every cycle. Compared to the first version, the performance increase, flexibility, and elimination of existing errors in the Blazor are incredible. If you never heard it, I highly recommended to try it.

THE END & GOOD BYE

“If you have read so far, first of all, thank you for your patience and support. I welcome all of you to my blog for more!”

Source Code:
https://github.com/borakasmer/BlazorWordGame

Video (Turkish):
https://youtu.be/pBdJ41InTUc

Source: Stackoverflow, MongoDB, Blazor, Microsoft, Peug.net, Blazor-university.com, Stackoverflow

--

--

Bora Kaşmer

I have been coding since 1993. I am computer and civil engineer. Microsoft MVP. Software Architect(Cyber Security). https://www.linkedin.com/in/borakasmer/