Introduction
Linux is a OS you can get from everywhere, it works very well on Internet. In this article, we will try to build a small online game website using ASP.NET Core. At the end, we will copy the Release to a new Windows system which has no .NET Core runtime installed.
Hello .NET Core
Before beginning a .NET Core website, first we are going to feel it on Ubuntu Linux. Without IDE, we use Command-Line to create a Console Project.
$dotnet new console
It created two file, the project game.csproj
, the CSharp file Program.cs
. the obj
folder you can delete it if you added a Dependency but got error.
Run the project
$dotnet run
Done. how do you feel, that is how to create a project without a Big IDE, using keyboard instead of using mouse.
But... without an all-in-one IDE, how to create database application? In this article, we will use Visual Studio Code to edit code, the record of Table and the object of C# are the same for the database engine, editing code is editing database, we will talk about database later.
Create ASP.NET Core WebAPI
We'll use WebAPI + JavaScript to build the game Website, creating WebAPI project is like creating Console project.
$dotnet new webapi
As most WebSites, we need two components, database and service.
First, add a database to the project, run
$dotnet add package iBoxDB --version 2.15
it adds the package information to game.csproj
, you can write the PackageReference
by hand. Code is written in bold in the following snippet.
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="iBoxDB" Version="2.15" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
Some projects cannot connect to Internet directly, we can download the Assembly from Web and copy to the project, then modify the game.csproj.
<ItemGroup>
<Reference Include="iBoxDB">
<HintPath>/home/user/Downloads/iBoxDBv21500_27/NETDB/iBoxDB.dll</HintPath>
</Reference>
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" />
</ItemGroup>
After setup the database iBoxDB
, we write nine lines of code to test it, and take a look the Editor, be prepared, Visual Studio Code
only inherits the name from Visual Studio
.
using iBoxDB.LocalServer;
public class User
{
public long ID;
public string Name;
}
DB db = new DB(DB.CacheOnlyArg);
db.GetConfig().EnsureTable<User>("User", "ID");
AutoBox auto = db.Open();
auto.Insert("User", new User()
{
ID = 1L,
Name = "Hello .NET Core"
});
Console.WriteLine(auto.Get("User", 1L));
ASP.NET Core is self-hosted, the database iBoxDB
is embedded, the console shows the DB is running and the WebServer is listening on "http://localhost:5000
". Everything works.
This WebSite is going to provide NES Game online Service, player can play NES game online. we copy the NES emulator nesnes
to ./wwwroot/nesnes folder, it is written by javascript. And create a new folder
./wwwroot/roms for the Game's roms, you can find these roms(*.nes
) on Internet, download and copy to this folder. Some games are not supported by the emulator, copy the NES Roms(*.nes
) to the folder as many as possible.
Setup is finished.
Get it Online
For now, the self-hosted WebServer only can be accessed through localhost
, and decline the HttpRequest to get *.nes
.
Modify Program.BuildWebHost() to listen any IP.
public static IWebHost BuildWebHost(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseKestrel((opt) =>
{
opt.Listen(IPAddress.Any, 5000);
})
.ConfigureLogging((logging) =>
{
logging.SetMinimumLevel(LogLevel.Warning);
})
.Build();
}
Modify Startup.Configure() to accept all files
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseDeveloperExceptionPage();
var fs = new FileServerOptions();
fs.EnableDefaultFiles = true;
fs.EnableDirectoryBrowsing = true;
fs.StaticFileOptions.ServeUnknownFileTypes = true;
app.UseFileServer(fs);
app.UseMvc();
}
Web Site Development
Most websites have a member system, we create a class User
to represent the player.
public class User
{
public long ID;
public string GameName;
public string ImageURL;
public DateTime Time;
public long Ver;
}
the GameName
is what game the player to play, the corresponding javascript code below.
var emulator = new NesNes(nesnes);
var xhr = new XMLHttpRequest();
xhr.open("GET", "./api/user", true);
...
var romPath = "./roms/" + user.gameName
emulator.load(romPath, true);
the Client(player's Browser) sends a HttpRequest to Server to get what game he can play, then Load() the game from server.
"./api/user
" is a ASP.NET Core WebAPI, the C# code below, it sets the GameName randomly.
[HttpGet()]
public User Get()
{
User u = new User();
u.ID = App.Auto.NewId();
u.GameName = App.Roms[u.ID % App.Roms.Length];
App.Auto.Insert("User", u);
return u;
}
We will have an Admin to monitor online players, he needs the Client sends the game screen(as image) back to the Server, and stores the screen in Database by using User.ImageURL
property.
var xhr = new XMLHttpRequest();
xhr.open("PUT", "./api/user/" + user.id, true);
xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify(user.imageURL));
Players are in and out, what the Admin wants to see, only the online players. how did we know which is activated. An easy way is adding a Time
property to class User
, when updating the Screen, updates the Time, then Select<User>( "from User where Time>?", lastTime) to get newest data from last time;
We know it will have many players online, two players may get the same Time when updating the Screen, let's take a look at the following scenario.
LastTime = 0
...
Thread-1::Player-1: GetTime() //Time=1
Thread-2::Player-2: GetTime() //Time=1
Thread-1::Player-1: Update()
Thread-2::Player-2: Update()
Select Time > LastTime
Set LastTime = 1
...
Thread-1::Player-1: GetTime() //Time=2
Thread-2::Player-2: GetTime() //Time=2
Thread-1::Player-1: Update()
Thread-2::Player-2: OS Suspends this Thread for some background tasks
Select Time > LastTime
Set LastTime = 2
...
Thread-1::Player-1: GetTime() //Time=3
Thread-2::Player-2: OS Resumes this Thread //Time=2
Thread-1::Player-1: Update() //Time=3
Thread-2::Player-2: Update() //Time=2
Select Time > LastTime //Who is missing?
99.99% of accuracy is fine for this small project, but can we get it better? iBoxDB
supports UpdateIncrement
, it means the Value of Field(long
type) can be increased when the object is updated, with thread safe
. To enable this feature, add one line of code at Config
DB db = new DB();
db.GetConfig().EnsureTable<User>("User", "ID")
.EnsureUpdateIncrementIndex<User>("User", "Ver");
Now, we can store the player's game screen on Server, we don't need to set User.Ver
, it will be set by the DB.
[HttpPut("{id}")]
public string Put(long id, [FromBody]string value)
{
using (var box = App.Auto.Cube())
{
User u = box["User", id].Select<User>();
u.ImageURL = value;
u.Time = DateTime.Now;
box["User"].Update(u);
CommitResult cr = box.Commit();
return cr.ToString();
}
}
Because every object has a Version
, we can get updated objects like this
while(true){
objects = Select<User>("from User where Ver > ?", lastVersion)
if ( objects.Length > 0 )
lastVersion = objects[0].Ver;
}
Get the newest Version at the beginning.
lastVersion = App.Auto.NewId(byte.MaxValue, 0);
In this article, we use Time Property at the beginning for better looking, because at beginning the updated objects is empty.
public IEnumerable<User> Get(long last)
{
int count = 4 * 5;
if (last == 0)
{
DateTime dt = DateTime.Now;
dt = dt.Subtract(TimeSpan.FromSeconds(60 * 1));
return App.Auto.Select<User>("from User where Ver>? & Time>? limit 0,?", last, dt, count);
}
else
{
return App.Auto.Select<User>("from User where Ver>? limit 0,?", last, count);
}
}
Although we used Time
at beginning, the Ver
Property is still needed, it told the DB to use Ver-Index to search, otherwise, the DB will use ID-Index to search.
PS: The "limit 0,count
" sentence above was used to debug Javascript & CSS, returning the Size we wanted to test Page Layout. Remove the "limit
" before publishing.
The corresponded javascript code
last = users[0].ver;
var divs = document.getElementsByTagName("div");
for (index = 0; index < divs.length; index++) {
for (var i = 0; i < users.length; i++) {
if ((!users[i])) continue;
if (divs[index].id == "u" + users[i].id) {
var img = divs[index].childNodes[0];
img.src = users[i].imageURL;
img.ver = users[i].ver;
...
break;
}
}
}
for (var i = users.length - 1; i >= 0; i--) {
var div = document.createElement("div");
...
document.body.insertBefore(div, document.body.firstChild);
}
for (index = 0; index < divs.length; index++) {
var img = divs[index].childNodes[0];
if (img.ver < (last - 100)) {
divs[index].parentNode.removeChild(divs[index]);
}
}
The final admin.html
Page
$dotnet run
Publish
We developed the Website on Linux, if we want to deploy it on Windows, edit game.csproj,
add win10-64
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<RuntimeIdentifiers>win10-x64;ubuntu.16.04-x64</RuntimeIdentifiers>
</PropertyGroup>
Then run
$dotnet publish -c release -r win10-x64
You will get a Windows Release in project/bin/release/netcoreapp2.0/win10-x64/publish, don't forget the /publish
folder.
It is a EXE file, means you can run on Windows directly. All .net core runtime are included in the package.
Summary
Cross platform is getting attention from developers, write once, deploy wherever customers like, no need to find someone to setup a system. Many programming languages, IDEs, tools and databases support Windows & Linux & Mac & Mobile devices, working very well. this article introduced a simple and clean solution by using .NET Core runtime with iBoxDB database, plus a javascript emulator Nesnes, you can use a business related emulator instead of game emulator to keep the product awesome!
Resources
iBoxDB -Database
Nesnes -NES Emulator
History
Version 1.0
Version 1.1