Подія відбулась
Подія відбулась

Квитки на наступну конференцію Конференція .NET fwdays'24 вже у продажу!

Making boring old WinForms game fun and cool with latest .NET features and cloud

Презентація доповіді

20 years ago I wrote a small WinForms game. All was done probably in under 2 hours. But .NET is different .NET now than it was before. Can I take some cool up-to-date technologies, cloud included, and make the game fun and cool? Let's find out...

Jiří Činčura
Microsoft
  • Software engineer at Microsoft.
  • Jiří Činčura is .NET, C# and Firebird expert.
  • He focuses on data and business layers, language constructs, parallelism, databases and performance.
  • For almost two decades he contributes to open-source, i.e. FirebirdClient.
  • Frequent speaker and blogger at www.tabsoverspaces.com
  • Mastodon,GitHub

Транскрипція доповіді

Good morning, good afternoon, good evening, everybody. My name is Jiri Chinchura, and in the next 40 minutes, I will be talking about a game that I wrote basically 20 years ago. So the title of my talk is Making Boring Old WinForms Game Fun and Cool, with Latest .NET Features and Cloud. As I mentioned, my name is Jiri Chinchura. If you want to follow what I'm doing and what's happening around me, you can go to tabsoverspaces.com, where you will find, obviously, some interesting stuff that I think is interesting, and I need to put it out. You can also find more about me, and you can find other ways to contact me in case you are going to have questions after the session or something like that.

Also, a little notice. This is a prerecorded session because sadly, as you are watching this session, I am somewhere in the middle of Austria, probably going for my vacation. I will try to check your questions that were posted during the session, but I cannot promise too much. So in case you have a really burning question or something you want to have really answered, it's more like a question and less like a feedback or a comment. Please feel free to go to my website, tabsoverspaces.com, find whatever communication channel works for you, and send it my way.

So, let me immediately jump into the code. And as I mentioned, this game that I wrote is really, really old. I would say 20 years. Let me give you a little bit of history, how this actually happened. It was the year 2005, and I know it was exactly 2005 because I subscribed to a class at the university. It was about .NET and CLR and other stuff. I remember a couple of classes we spent digging into generics and IEnumerable and IEnumerator and all the stuff around it because generics were kind of the new thing. So I guess it was around the year 2005. Part of this class was also like practice sessions with homework and other stuff.

At some point, we were doing something with WinForms and doing some simple programming and events and other stuff. But because I was kind of experienced with what we were doing, I was bored, and I started doing something else. Something else is basically the game that we have here today. Let me run it so you actually see what it's all about. It's a simple window. It draws a bunch of lines and connects them together. Your task is to drag these edges and put them in such a configuration that there is no crossing between the lines. As you can see, the game does not give you the indication of whether you are done or not. I remember when I was writing it, I had like two hours. This is like, well, the class was for two hours. I don't remember exactly whether I had two hours. I probably was doing the assignment initially. But once I was done and bored, I started doing something else to kill time. So let's say I have an hour and a half.

I remember a couple of years later, I was doing another class, and it was about some mathematical stuff about like polygons. Basically, like shapes and figuring out, 'Hey, am I inside? Am I outside? What is the easiest way to get to the border between these two objects? And if I go to the left, does it mean my left hand is inside or outside?' All these different algorithms. It was a really fun class because I thought that it was really a practical use of mathematics to apply into this one. One of the algorithms that we were kind of studying through the lenses of the math, not through the lenses of programming language, but nonetheless, was basically figuring out whether you have, I think it's kind of closed or not closed a polygon.

Basically meaning whether you have something like this, we were interested in. Do we have a polygon that's like part of another polygon, which basically means it's crossing somewhere? I remember at that time I was like, oh, that would be nice to update my game. I never did. I think I also never did before because I was kind of expecting that math basically tells you, hey, check that these lines are parallel or that there is an intersection or something. But once you start doing it with computers and lines, you immediately go into rounding and rendering, and suddenly you figure out, you realize like the pixels are not aligning absolutely totally perfectly. So you need to have some buffers and boundaries and other stuff. Basically, hey, if it's less than 0.0001, that basically means it's touching because it's never going to be exactly zero and so on and so on. So this is the game. The task is to make it that there is no crossing between the lines and make it with as little moves as possible. The moves are shown in the title bar.

The first thing that I was really surprised in a positive way was that I took this code and copy-pasted it into a fresh WinForms application that's using basically .NET 4.8. But with all the stuff that we experienced with .NET 6 and .NET 7, I could use .NET 6, 7, and whatnot, but I used .NET 4.8 as the latest .NET framework that is there. Then I just did a little bit of fine-tuning. You can see that definitely in 2005, lambda expressions were not a thing. So yeah, a little bit of touching on the code to make it a little bit more succinct.

The game itself is pretty easy. What I have is a list of points or an array of points. And basically, a method called drawGame that draws the lines and the circles so you can move these pieces. Then I have a simple event that when I'm beginning to move, when the mouse is clicked and I'm checking whether I'm inside the boundary for the point, basically, if so, I remember the index. I say, hey, I'm moving, and that's basically it. As the point is moving, I'm moving the coordinates. I'm moving the .NET and I call invalidate, which basically means the draw is going to be, the game is going to be drawn again. If I lift the mouse, then I say moving false, and that's about it. So it's not a huge piece of code. I think at that time, the biggest time-consuming part was drawing the circles in a correct position and so on because different UI frameworks draw circles slightly differently and so on and so on. Anyway, if I run it, it's fine. I remember I spent countless hours trying to kill some time with this game.

As you can see, it always draws a random character. Obviously, you can change the code so it always draws the same structure. With this one, using only 10 points, it's pretty easy. But once you go into like 40 or something like that, it becomes a little bit more difficult to untangle this one. Probably it would be easier to have one full screen, but basically make it more difficult by making more points and possibly making it a little bit bigger. But you know, this is a WinForms application. That's not cool. Desktop applications are not cool, and I want to update it. I basically said, hey, what does .NET have to offer for me that would allow me to make it a little bit nicer? Obviously, being in WinForms, and I don't have anything against WinForms, I think it's still the best way to create Windows desktop applications, mostly because that's the only UI framework that I ever used extensively. I kind of know my ins and outs. I never used WPF that much. I never used the Universal Windows applications and Maui and other stuff.

So that's the reason why I'm saying that WinForms is basically the best one, because that's basically the only one that I know. But still, if I want to have this game for you, for me, for other colleagues, friends to play, this is probably not the best one to do it. Although I can take it, I can zip it, and send it to someone and can run it. It's still not going to be super nice. So what I did then, I said, well, okay, let's create a website because now what I can do is pretty easily create a website, even on Linux, and run the game. So that's basically what I did. I created an index page, which is basically a canvas, and a JavaScript file. Yes, a JavaScript file. I'm not super keen on JavaScript and other stuff, but I was pleasantly surprised that the code was more or less copy-paste. I copy-pasted it and then in half an hour, it was working. It's not exactly the same stuff, but it was pretty easy. I didn't go into TypeScript and other stuff, and I'm pretty sure it would be probably a little bit easier. But again, for, let's say, a hundred and something lines of JavaScript or C#, it's okay-ish probably to just write it this way.

I was surprised how close these languages are in terms of working with algorithms. We used to argue about this, this language is better than this, and so on and so on. But what you realize is it's not about the language. The language is pretty easy. It's about the ecosystem around. If I would now want to connect to, let's say, an MQTT broker, well in .NET, I have all the features with sockets and TLS and other stuff. With JavaScript on the browser, it will be a little bit different story. I can still use web sockets and TLS and other stuff, but suddenly, I don't have the full ecosystem of .NET.

But there is something that I would like to mention. I can't replicate it because I already did. So it would look like cheating, but maybe, you know, a tool that's called Copilot, GitHub Copilot, that basically is trying to guess what you are trying to write. As I was writing my code or changing the code, I had to create this method called drawCircle. And this method creates the blue circle around the point where you can drag and drop. One problem that I experienced is that the API for drawing on a canvas is slightly different than drawing circles or arcs in WinForms. And sorry, if I switch the story or switch the fact a little bit, I don't remember exactly. In WinForms, when you are drawing a circle, you are specifying a center point and then the arc, basically.

With the canvas, you are specifying the top left point, if I'm not mistaken, and then you are drawing an arc. Again, this is simple math. Basically, you just need to shift it and do the other stuff. But as I was writing it, I remember Copilot. Basically, when I created the drawCircle signature, it immediately offered me the method. And now I realized that Copilot is actually pretty interesting. I can try to do it, but obviously because I already did and the method is already there, it's just going to be not super interesting. So what I did is, and you can see it's now trying to tell me, hey, maybe you want to draw text because you already have a line and you have a circle. So maybe you want to draw text. But for me, I was trying to draw a circle and you can see it's guessing what the next thing is going to be. And you can say, hey, it's the same stuff as it's above. Yes, that's exactly what it is.

But it's kind of like offering me what should be there. In this case, it's exactly the same code as it's above. But when I started it, first of all, it offered me the full method, and it was more or less correct. I don't know whether it was absolutely correct or not, but it was there, and I was surprised that it was working nicely. So that's cool technology. And I realized that it's probably not about your primary language, something you are doing day in, day out because if I turn it into my libraries, in my daily code, in C#, it's usually not offering me super helpful code.

It's something that I could write really easily without waiting for it or without looking at the screen. I can just keep typing, and it will be faster. Or it's so difficult that it has no idea what I'm actually doing, but it's not about that. It's about, hey, if I need to switch into JavaScript, if I need to switch into Python, if I need to switch into, I don't know, parallel, something that I'm not super familiar with. I still know my ins and outs, but there might be these small differences between how something is computed, how something works, what's the name of this class, like math.pi and other stuff. I can find it. I can Google it. I will be just slightly slower with the Copilot. I will be at the same speed as I am in C#, but I know every piece and every corner I ever need. So that was pretty nice.

But you know, I can run it and oops, wrong screen. And this way and it works kind of so I can move it and I can play with it, but it's still kind of rough. You know what I mean? I ported the WinForms into the browser, and that's about it. Then I said, well, what next? This is really not super interesting. Yes, I can now run it. I have Kestrel. I have the web server. I can package the application, send it to somebody else, but it's still more or less the same. The biggest problem I have with me programming the website is that I don't want to be in JavaScript playing with the DOM and changing the DOM and adding a button here and then the button there and dynamically creating the UI. I think nobody likes to do this. That's why we have all these different JavaScript frameworks and UI frameworks and so on and so on. But I want to stay in .NET in JS world as long as possible. So what I have here is something that's called Blazor. Let me set it as a startup project.

Blazor is actually using C# and WebAssembly as a way to create your website or your web application. What I have here is basically a piece of HTML. You can see there is a button and there is a break and there is a canvas, but there's also the canvas after this condition that basically says, hey, is the game enabled? Yes. No. If not, then I don't want to see the canvas and whatnot. Then I have a bunch of code. It's basically doing the same stuff. It's doing a little bit more. I will get into this in a minute.

But what is interesting is that it's basically .NET code using the game logic. This is the begin move, and this is exactly the same stuff. It's copy-paste of the original code with some minor tweaks, but I still save the index, and I still say, hey, I'm moving. If I am done moving, I will just set it to false. And if it's moving, I just call a method called do move. I will give you the game ID a little bit more about it and the index and the X and Y coordinates. The only problem for me is that I'm actually using something that's deeply integrated into the HTML. I have a canvas, and I'm basically drawing something on the canvas. This is the place where Blazor is not going. To help me because Blazor is good. If I'm creating like HTML elements and putting them in there and doing some interaction validation, moving, lifting, hiding and so on and so on. But still, I have most of my game logic back in C#. I have a little bit of stuff around here. That's for the future. You can ignore it. And then I have a small interaction between the game.

So if I go into my HTML file, you can see that I have a few methods that I need to be able to call. So I have this init game and I have this draw game, and the draw game is doing the same stuff as was the previous code doing. It's drawing the lines and the circles. As I mentioned, this is not where Blazor is going to be any helpful because I'm deeply in HTML and canvas stuff. So the rest is basically the same. It's again clear canvas, draw line, draw circle, same stuff. I just copy-pasted it. And that's what it is. If I would create it from different elements instead of drawing on canvas, I would maybe have less JavaScript whatsoever. And then I'm also calling back into the Blazor. So what I have here is the invoke method async, and I am calling the point begin move, moving and end move is in here. So I'm basically calling back into Blazor. And as I mentioned, Blazor is really interesting. It's really interesting. Because it is a UI, let's call it a UI framework. It's running on the web, but it's using WebAssembly. This kind of like a universal language that we can use. So I can still stay in the C#.

And obviously it's not only this, you can do a lot more. But this is really, really interesting. So let me just quickly check what is happening in my settings should be okay. So let me run it again. Run window and says I'll handle the error. Okay, something else. Oh, I think I know what it is. It is, it is, it is this server address. Let me check this one. Yes, and I think this is not correct. I think we need to have this one as well running. So let me set it to start up. And I have to do for yes. Okay, let me put back the Blazor. And now it should be. Okay. Okay. So there it is. So now it's loaded. You can see that I added a huge improvement in the UI. Well, at least, for my design skills, I created a text box and a button that allows me to load a game. We'll go into that in a second. But basically what I can say is test and say load game, and it loads me the game with the identifier test. Oops. Why is the browser not cooperating with me? You know what I want to do? I want to make it slightly bigger. This way. And again, I can just start moving these points and have fun with this.

And again, if I reload it, there's no other stuff in UI. I type test and it loads the game for me. So this allowed me to minimize my exposure to JavaScript and to basic HTML to the minimum, because what I'm creating and what I'm using is a bunch of components. Razor components, razor pages, and I don't have to fiddle with these small stuff left and right and just using what's enabled and what's provided for me. So normal C#. I have all the power of the C# added available. Obviously, I'm still running in WebAssembly in the browser. So I'm constrained by the environment. I cannot, let's say, allocate like 15 gigabytes of RAM, and because I'm running in WebAssembly, it's not super fast compared to running on bare metal, but still, it's running in the browser. I don't need an executable. It just loads, and that's about it. And I'm doing it in C#.

So if you have a lot of logic, a lot of validation, a lot of business logic, and basically like a line of business application, then Blazor is absolutely a great fit. What I would like to point out is that on this button, I have this onclick load the game, and what this load game is doing is checking some stuff initializing the game, yada yada yada, and then it has explicitly calling here StateHasChanged. I need to call explicitly because I am changing it in here and I need to load it. I need to load the canvas immediately. Normally when you are changing the UI, it kind of happens automatically, but I'm pretty sure you saw Blazor or you can check like the basic demos on the Blazor. But as I am setting this to true, I'm not doing any modification with the HTML just because the property changed. The canvas was rendered onto the UI. It's not like I was reloading the page or something like that. It just shows that it's running completely on the client. Let me run it again and show you a small piece. That's actually happening.

So if I look into my network and reload, I can actually see there's a Blazor WebAssembly dot JS and other stuff now because I'm running into the debug and stuff. It's like hot reload and other stuff, but basically says hey, it loaded 10.38 megabytes of resources in this case from the cache. And again, I'm running in debug and other stuff. If you publish your application, everything is like way way way smaller, but I'm running completely client-side. Once I get all these like DLLs and other stuff, I'm done and I don't have to worry about being connected. It's completely running client-side, which is pretty nice because it suddenly allows me to have like fully client-side application without me learning another UI framework in JavaScript or whatever else might be hot today. As I mentioned, WinForms, good for me.

Then I kind of skipped everything and went into HTML. Or I think I knew HTML even before WinForms, but I mean like pure HTML. So as far as I can use just pure HTML, I can find my way around. If it becomes CSS, it becomes a little bit more difficult. Definitely don't ask me how to center div left and right and right and left and top and bottom and other stuff. But HTML is kind of like the universal language. So that's fine. And if I can create a decent UI in it or maybe even a game, then it's absolutely fine with me. So, well, I have a web-based application with most of the logic still in C# and I can play it. But as you can see, there's a little bit more happening in here. And the reason why a little bit more is happening in here is that I actually wanted to be able to play it with other people. The original game, you just started your WinForms application and you challenge basically yourself. You can have the fastest time, lowest number of moves and so on and so on. It's up to you.

But what I actually created is an option to play with other people, kind of a multiplayer game. So what I have in here is a website that I was actually starting before that is using another cool technology that you can use in .NET, and it's called SignalR. SignalR basically allows me to use WebSockets to do bidirectional communication between connected parties, between clients. I'm pretty sure you saw the demos where you can create a chat and other stuff. But what I wanted to do is basically have an application that allows me to send these moves to some central location and the central location then distributes it into different browsers or different clients.

And we can kind of play together. So that's what I did. This website is basically the same stuff as before. The only difference is that it's using this game hub. And this game hub has three interesting methods: StartGame, that basically is going to create the layout and send it to all the connected clients. Then StartMove, so we actually see that some movement is happening and it's happening in real time. And then DoMove. That finishes the move completely. And that's about it. At the end, it always sends refresh to the game. Let me make sure that it's always running these two together. So let me do the Blazor and SignalR. Now if I run it, again it opens in the wrong window. So let me fix that. I have now the web-based version, the same as before. And then the Blazor version. The web version immediately starts a new game and gives me some identifier. So if I copy-paste it into Blazor, it loads the same layout, the same game. And then I can take a point and I can start moving. And I immediately see it on the other side. So now we have a multiplayer game. And the only thing that was needed was a couple of lines of code. And I will show you what's happening in there.

So let me close this. The backend part is pretty easy. I just derive from this hub and create, in my case, GameHub. Then I have a bunch of methods. It doesn't really matter what it is. It's just a bunch of methods that I can call from the client or from other parts on the game. And I can pass whatever arguments I need. And then I have this Clients object. And this Clients gives me an option to send it to all clients, only to caller, and then all except, and clients, and do some little bit of filtering. In my case, I'm using groups. And the groups are the groups for the game. So if like five games are being run, I'm only sending the new points into the game. It's actually run. This is the ID that I'm actually sending inside. And it's also the ID that's in the URL. On the code side, it's pretty easy because the only thing I need to call is this MapHub method. So I just say, hey, there is a GameHub on this URL. It's implemented by this one. And that's about it. From the code point of view, I still have my old JavaScript code as part of this project.

But if I run it in Azure, then storing it somewhere like a database in Azure would be really easy. So let me show you how it's actually implemented. So in this case, I created an Azure Function. And this Azure Function is relatively simple. It's just like an HTTP trigger, and it's saving the game. In this case, it's doing a blob storage and saving a JSON file. So if we look into the code, it's just a function that's getting an HTTP request. It's getting the game ID from the query parameters. It's getting the content from the body, which is basically the state of the game. And then it's saving it into a blob container using the game ID as the blob name and saving the content as the blob content. And that's about it. It's a very simple Azure Function.

On the code side, what I did is when I start the game, let me show you the Blazor part. In the game hub, when I start the game, I also call the save game function. So I call this save game function, passing the game ID and the content of the game. And this save game function is an HTTP client that's just sending a POST request to the Azure Function. So I have the URL of my Azure Function. I pass the game ID as a query parameter, and then I pass the content of the game as the request content.

So in this way, every time I start a game or make a move, I'm also saving the state of the game into an Azure Blob storage using an Azure Function. And then later on, if I want to load the game, I can call another Azure Function that retrieves the game state from the Blob storage and sends it back. This way, I have a simple way of persisting the state of the game and also retrieving it when needed. And it's all happening in Azure, so it's scalable and can handle a decent amount of traffic.

Let me run it again and show you how this works in practice. So I have the web version and the Blazor version. Let me start a game on the web version, and it gives me an identifier. Now, if I copy this identifier and load the game in Blazor, I can start moving points. And if I go to the Azure Storage Explorer, I should be able to see a blob with the name of the game ID. This is the Azure Storage Explorer. I'm connected to my Azure Storage account. And if I go to the "games" container, you can see that there's a blob with the game ID. And if I open it, you can see the content of the game, which is a JSON representation of the game state.

So this is how it's being stored in Azure Blob storage. And then later on, if I want to load this game, I can call the load game function, which retrieves the game state from the Blob storage and sends it back to the client. So this is a simple way of adding persistence to the game, and it's using Azure Functions and Blob storage for scalability and ease of use. This way, you can have multiple users playing the game simultaneously and persisting their game states in the cloud.

Definitely, I need to back up and do other stuff. So, I decided, hey, I will put it into the cloud. I'm storing it in blob storage, which is super cheap, super fast, super reliable, and durable. And I created this Azure function. The reason why I created this Azure function is that it allows me to scale. Basically, it allows me to have the number of servers needed to handle the load. So, if there is nobody playing, there will be no server and no charge. If there are a thousand people playing, there will be, I don't know, 20 servers, and they will handle the load, hopefully fine unless I make a mistake in my code.

The same story applies to my WebSockets. If you ever try to handle WebSockets, it's fine if you have 10, 15, maybe 20 WebSockets. But once you go into hundreds and thousands of WebSockets being open, it's not super easy, mostly because you can easily exhaust resources on the server. It's also difficult to keep them open all the time. For example, if I were to redeploy my application, all the WebSockets would be closed. Obviously, clients would reconnect back, hopefully resume the game. But it would not be a super nice experience, especially if I were to disable the gaming board or something like that. Then I might have problems once all, let's say, 10,000 clients start to reconnect. It will hammer my server. It will probably be a little heavy on my server, especially if I want to publish it into something smaller and just use the cloud to handle the heavy load.

So what I can do is actually use SignalR in Azure. That's what I did in the last project or second project, the last project that I have in here. What I have in here is called SignalR and also Azure SignalR. I provide a SignalR connection string. In this case, the client is not actually connected to my server. It's connected to Azure. So I don't care how they scale it, whether they handle thousands and tens of thousands or millions of connections. That's really not my business. In the best-case scenario, the only worry I have is, well, I just need to scale up or something like that, or it will do it automatically even better.

That means I'm actually just serving the HTML, doing the initial handshake to provide, 'Hey, instead of connecting to me, connect to Azure,' and storing and loading the game is done through the Azure function again in Azure. So I don't have to worry about it. The experience will be the same. For the sake of time, I'm not going to run it. The only thing I will also show you is that let's say I want to take this application. Why is it preferring that screen? And I would like to publish it somewhere. And make it a little bigger. As I mentioned, I would probably be able to publish it into the cloud as well. Then I have the WebSockets being handled by the cloud. The storing and loading of the games will be handled by the cloud. And obviously handling my game.

But I might say, well, what if I want to have it locally for some reason and only the heavy part like the WebSockets and the storing and loading would be done in the cloud. So what I can actually do is that's about it. I can call .NET publish and say, 'Hey, I would like to publish it into Linux arm.' The reason why I'm doing this is that I can then take this one and put it on, for example, Raspberry Pi. And. Copy-paste this one. If I go. Oops, not correct.

Why is it not copy-pasting everything? I know there is a space. It's formatted. Good old notepad. Fix that. And now I can take and deploy this application to my Raspberry Pi. But, as you can see, there are again a lot of files. And I want to copy it over to a server. I want to put my CPE onto my Raspberry Pi and run it. So, I will do another small trick, and it will be publish single pi equals true. And now, it will have just a single file. Well, almost just a single file. There's a JSON for the libraries. I don't need that. PDB. I don't need the PDB for debugging, so I can delete that. I have one file, and obviously www root, with static files like JavaScript, and CSS files, and other stuff. So now I can take the directory and the executable, copy it into the Raspberry Pi. Run it. And I have everything up and running. And even if somebody needs it, like run it locally, they can just run the binary.

And the rest of the heavy lifting, let's say, like the web sockets and the storing and loading are still handled in the cloud. So with that, I'm kind of running heavily out of time. This is all I have for you today. For me, it was definitely fun. Playing with code that was 20 years old. And improving it. And moving it into, let's say, 2023. So to give you the summary, we moved it to the web using Blazor. Then we used web sockets to have a multiplayer game. We stored the game, so they are actually somewhere that you can look at the history or the result or start again using the Azure functions. And we stored it in the Azure blob storage for absolute durability. So it's never going to be lost. Well, unless somebody clicks badly in the portal and deletes something or something like that. I'm out of time. If you have any questions, please feel free to send them to me directly. I will also try to check later when I have time and when I arrive at my final destination, the conference chat. And if there is something that catches my eye and it's still kind of open for responses, I will try to respond there. With that, thanks for watching. And hopefully see you next time.

Увійти
Або поштою
Увійти
Або поштою
Реєстрація через e-mail
Реєстрація через e-mail
Забули пароль?