• Advertisement
Sign in to follow this  
  • entries
    4
  • comments
    4
  • views
    3766

About this blog

Notes on my journey from a software developer to a indie game developer

Entries in this blog

After having spent a month or so on my UDP server, I decided it was time to consider it good enough and move over to the client development. First item of business over there would be the creation of the renderer using an Isometric perspective. I was able to get one setup using some sample tiles I found online and a tutorial on Isometric rendering with XNA.

WIucT7R.png

The assets used in the screenshot are just sample assets. I think this tile size is a good fit for my game; I just need to create some assets that are more appropriate for what I'm developing.

I was originally planning on developing the game using Cocos2d-x, writing it all in C++. However, in order to prototype a quick client that could interact with my server I decided to build something simple using MonoGame and C#. After the exercise of creating the Isometric perspective and the math involved. I realized just how much I had to learn in regards to game development. While I've programmed now for nearly 20 years, none of it has really been game development. What I didn't want to do was learn both game development and C++ at the same time. Doing that felt like a huge learning curve that would potentially end up with me giving up in frustration and moving back to my non-game development projects. If I stuck with C# as my language for the game, then that would really help me focus on just learning game development, independent of the language. So MonoGame it is for this project. I would really like to build a game in C++, so perhaps I can take my experience with iMini and learn C++ on my next game.

While writing this code in MonoGame, I've learned a few things. The first thing is that it is extremely easy to just pile all your logic updates and rendering updates into the XNA Update() and Draw() methods within the Game subclass. My Draw method is a hideous monster of crap. The good news is that i know what needs to get refactored out of it and how, I just haven't done it yet.

The second thing I've learned is that there's a lot of math involved. A lot. I know 3d games have a ton of math; I guess I figured that a 2d game would just have really basic math. I didn't realize until I started building the Isometric perspective just how much math was involved. It is still basic math, but just a lot more of it than expected.

The third thing I didn't realize was that at 60fps, you only have 16ms to render the frame. At 30fps you have a bit more time (33ms) per frame but not much. So I'll have to be careful as I write my code. I had figured that a 2d game wouldn't need to worry about performance at all. I thought I'd just write my code in the easiest way possible and not worry about perf. If I only have 16ms per frame to render and update logic, there's a lot of optimizations I can see needing to be done over time. If this game wasn't a multiplayer game, where I'd have to update all the players locally, then I wouldn't be to concerned. The networking pieces will require some careful design when I get to it on the client.

EOF

So the server has started taking some form over the last couple of days. I've got the datagram message parsing put together and have a test client app sending data to the server for consumption without issues. There is a lot of work still to do, like handle packet loss, deal with packet ordering and then deal with multi-packet messages.

There are a few things that need to happen when the datagram first arrives to the server.


  • Parse the contents of the message
  • Determine the channel that the message belongs to
  • Store the datagram if it belongs to a multi-part message, until all of the messages have come through.

    The parsing of the datagram was straightforward and was covered a little in my previous post. I've since done a bit of refactoring and cleaned up the way that the datagrams handle the binary data reading and writing. Now, the server creates the BinaryReader when a Datagram arrives. This allows the server to read the datagram header and route the message accordingly to the correct channel. Upon arriving at the channel, the channel can resume reading the binary stream from the position that the server left off at while reading the header. This allows me to not read the header contents twice, by sharing the same reader. I don't have channel routing in place yet, so I'm using a factory to handle instantiating the datagram message object and letting the server interact with it directly.private void ReceiveClientData(IAsyncResult result){ // Pull our the connection state and it's data PacketState state = (PacketState)result.AsyncState; Socket socket = state.UdpSocket; EndPoint endPoint = state.Destination; int receivedData = socket.EndReceiveFrom(result, ref endPoint); if (receivedData == 0) { this.ListenForData(socket); return; } // Create a binary reader so we can deserialize the bytes delivered into an IMessage implementation var reader = new DatagramReader(state.Buffer); // Read the header in from the buffer first so we know what kind of message and how to route. var header = new ClientHeader(); header.GetMessageFromBytes(state.Buffer, reader); if (!header.IsMessageValid() && header.IsRequiredMessage) { // Handle sending a request back to the client. // this.SendMessage(new ResendDatagramMessage(header)); } // Now we have the header, ask the factory to create the datagram message for us. // In this example, 'datagram' will represent a Handshake class. IDatagram datagram = this.datagramFactory.CreateDatagramFromHeader(header); if (datagram == null) { // TODO: handle null } // Tell the datagram to pull the data out of the buffer and populate it's values. datagram.GetMessageFromBytes(state.Buffer, reader); // Clean up and grab the next socket buffer if available. reader.Dispose(); this.ListenForData(socket);}
    This little bit of code is the current heart of my socket listener. There is a lot of work to be done here, such as caching the IPEndPoint for sending data back, creating a User object for future referencing and more. What I want to show here though is that the server doesn't need to do any switch statements based off of the datagram message arriving from the client. Instead, a factory is used to figure it out. Once the factory gives back a datagram, the server tells it to read the data from the packet so we can react to it.


    The factory itself is pretty simple. It is given a collection of every IDatagram implementation available for caching. It runs through the collection and maps the protocol attribute (shown further down) to the Type representing an IDatagram implementation. When the factory is asked to create an instance of the IDatagram that is defined in the header, it just pulls the correct Type from cache, based off of the Protocol Attribute key. A new instance of the IDatagram implementation is then returned to the server.public sealed class DatagramFactory{ private static readonly Dictionary datagrams = new Dictionary(); public DatagramFactory(IEnumerable datagramTypes) { foreach (Type datagram in datagramTypes) { if (datagram.IsAbstract || !datagram.IsClass) { continue; } ProtocolVersionAttribute protocolVersion = AttributeCache.GetAttribute(datagram); DatagramFactory.datagrams.Add(protocolVersion.DatagramName, datagram); } } public IDatagram CreateDatagramFromHeader(ClientHeader header) { Type datagramType = null; if (!DatagramFactory.datagrams.TryGetValue(header.MessageName, out datagramType)) { return null; } IDatagram datagram = (IDatagram)Activator.CreateInstance(datagramType); // TODO: Figure out how to assign the header without using reflection; it needs to be on the interface in some manor. datagram.GetType().GetProperty("Header")?.SetValue(datagram, header); return datagram; }}
    I needed a way to map the packet message to a version of the messaging protocol i'm using, along with define what channel it belongs to. I did that using attributes, and a caching strategy. During the server startup, I cache all of the IDatagram message implementations and their associated Attribute. This way, when messages come in, I don't have to use any reflection. I will pull the attribute and the Type information out of cache and instantiate it.[AttributeUsage(validOn: AttributeTargets.Class, Inherited = false)]public class ProtocolVersionAttribute : Attribute{ public ProtocolVersionAttribute(int targetVersion, string datagramName, DatagramChannels category) { this.TargetProtocolVersion = targetVersion; this.Category = category; this.DatagramName = datagramName; } public int TargetProtocolVersion { get; } public string DatagramName { get; } public DatagramChannels Category { get; }}
    This is what the Attribute looks like. It holds the protocol version, the name of the datagram and the category. This allows my factory to look at the message name passed in from the datagram packet, and find a matching IDatagram implementation.


    The following is an example IDatagram implementation. The handshake message would originate from the game client, and the server would consume it and turn it into a Handshake object.[ProtocolVersion(1, DatagramNames.Account.Handshake, DatagramChannels.Account)]public class Handshake : ClientDatagramBase{ public string WelcomeMessage { get; private set; } = "Hello"; public bool IsFreshStart { get; set; } protected override void WriteData(BinaryWriter serializer) { serializer.Write(this.WelcomeMessage); serializer.Write(this.IsFreshStart); } protected override void ReadData(BinaryReader deserializer) { this.WelcomeMessage = deserializer.ReadString(); this.IsFreshStart = deserializer.ReadBoolean(); } public override bool IsMessageValid() { return string.IsNullOrEmpty(this.WelcomeMessage); }}

    This works out pretty well thus far. Nice and clean, which lets me quickly add new datagram packet messages. It's not shown here, but the base class has a ClientHeader property, which represents the header we parsed out up above in the server code.


    So that solves for parsing the datagram packet content. What about determining what channel each message belongs to? Well I've used the attribute to define that but I've not done anything with it. I'm exploring multi-casting so that I can optionally route different channels to different IPEndPoints if needed. Something like a chat EndPoint that I could put on an independent box to handle the verbose chat traffic, and grouping non-verbose channels together on a single server box. I'm still working through the design on that one. The channel needs to be flexible in the event that I want to filter chat, or locations based on OS, Protocol Version and the contents of the message.

    The last item, storing of the datagram for multi-part messages, will be worked on over the weekend. I need to get the user connection class created and wired up with the server before i start dealing with multi-part messages though.

While I wait for some additional guidance on animations, and get some more experience with the artistic side of things, I decided to start working on my server-side code. The game is a social avatar styled game, similar to Second Life, but not quiet as large and done all in 2d with anime styled chibi characters.

The server will be written in .Net for no other reason other than that's what I'm comfortable with. I'm hoping the learn enough about UDP communications in a language i'm experienced with, before moving over to writing UDP client side code in a language I'm not experienced with (C++).

The server has a simple interface defined for now that holds the data that must be sent to the client over the socket.public interface IMessage{ byte[] GetMessage();}
This lets me implement the interface on multiple classes, with each containing the data that represents their message payload, and allowing the server to not care about what the payload is. It just needs the collection of bytes to send. An example of a message would be sending the updated X/Y coordinates of a character in the world to the client.public class LocationUpdateMessage : IMessage{ public LocationUpdateMessage(int x, int y, int userId) { this.UserId = userId; this.X = x; this.Y = y; } public int UserId { get; private set; } public int X { get; private set; } public int Y { get; private set; } public byte[] GetMessage() { using (var stream = new MemoryStream ()) { var writer = new BinaryWriter (stream); // TODO: Build a header object instead of sending along the message type here. writer.Write (this.GetType().Name); // Package up our location writer.Write(X); writer.Write (Y); writer.Write (UserId); // Get the bytes out of the writer. stream.Seek (0, SeekOrigin.Begin); byte[] data = new byte[stream.Length]; stream.Read (data, 0, stream.Length); return data; } } }
The server can listen to requests, update the characters X/Y location and then push the location update message down to any client that needs to be told of this change.


My very basic server only supports a single client at the moment. I spent the evening mostly just researching UDP, talking with a friend of mine about his experiences with UDP and binary serialization, and then threw this together really quick.public class Server{ private readonly UdpClient client; private readonly IPEndPoint destinationClient; public Server () { this.destinationClient = new IPEndPoint (IPAddress.Parse ("127.0.0.1"), 3000); this.client = new UdpClient (this.destinationClient, 3000); client.Receive (ref destinationClient); } public TransmissionInfo SendMessage (IMessage message) { byte[] data = message.GetMessage (); this.client.Send (data, this.destinationClient); return new TransmissionInfo (data, destinationClient); }}
This lets me very easily spin up a new instance of the location update message and send it to a client.server.SendMessage(new LocationUpdateMessage(253, 251, 6));
Obviously it'll have to get re-worked some to store a collection of destinations that represent all of the clients, handle a client time-out, and being given a character object that can be mapped to an IPEndPoint for publishing.


My next bit of work that I want to do is wire this up in a UI, that lets easily view a list of connected EndPoints, send them messages if I want and view stats such as their last transmission, packet sizes, time spent transmitting etc to help troubleshoot issues and performance. I wrote the server-side code tonight in OS X using Xamarin. I've not written a UI based app in OS X yet, so I might just go ahead and spin up my Windows 10 VM and build it out in WPF initially just to get me going. I like to follow the MVVM pattern, so if I want to move the tool over to a OS X UI, I can move most of my code without issue.

Until next time.

Well, this is something new. I usually post any kind of a blog entry on my own site, but for some reason it feels better talking game development in a Journal here at gamedev.net, rather than on my site. There's a sense of separation of concerns if you will, a nice way to isolate my thoughts on my new project.

Introductions

I suppose I should start with a bit of an introduction. My name is Johnathon Sullinger (@Scionwest on Twitter) and I'm a Lead Applications Developer at my day-job where I architect and build internal LOB apps. I exclusively use .NET and C# in a Windows environment there, and enjoy the work that I do. At home, I've programmed in a few different languages and tech, from Objective-C and Swift to cross-platform .NET development with Xamarin and the new .Net Core APIs. I've been programming for 20 years, starting when I was 10 in Visual Basic 5. Here I sit now with roughly 11 years of experience in C#, picking up another new language to learn. Today I set out to dig in and understand C++. I've written in it before, while writing Objective-C, it wasn't a lot of exposure though. Most of the time I skipped passed C++ and went to the C APIs.

What's being developed?

So what am I making? Well, I'm keeping that a little bit closer to the chest than I typically keep my projects, at least for the time being. I do however plan on sharing what I am coding and working on as I develop the game. I might not explain the core idea behind my game, but I do plan on having lengthy posts here in regards to what I'm working on, how I'm designing and developing the game and so forth.

Why C++ when my core competency is C# you ask? There are several libraries available in .Net that I could use, such as the aging XNA framework, the MonoGame framework or even a full engine such as Unity 3D. In truth, I did dabble a bit in each one of them. I've used Unity before and just never liked it. Their new pricing strategy doesn't float my boat either. I didn't want to target XNA, a deprecated framework, with a brand new game. I'd miss out on hooking into so many of the cool APIs that the various operating systems I am targeting supports. MonoGame I couldn't get working on my Mac, which will be my primary dev environment. So I settled on Coco2d-x and C++.

What I've done

I've actually not done much of anything yet. I did get a coco2d-x project created and compiled. I started working in Xcode and remembered how much I hated the IDE. While reading through a tutorial series on coco2d-x I discovered AppCode by JetBrains. Having used it all day today, I think I've found my permanent IDE while developing in OS X. It's a really solid IDE.

I created the initial headers and implementations of my games questing system. A Quest object, and an Objective object. Quests can hold a collection of Objectives that must be met in order for the Quest to be marked as completed (side-note, it's not an RPG!). I was pleasantly surprised to find out that C++ 11 has lambdas, for-each and generics (Templates I believe they're called in C++). I was thinking I would have to handle arrays with my own algorithms and it turns out the majority of my needs can be met with std::vector.touchListener->onTouchBegan = [this](cocos2d::Touch* touch, cocos2d::Event* event) -> bool { labelTouchInfo->setPosition(touch->getLocation()); labelTouchInfo->setString("You touched here"); return true;};
Using lambda's in C++ is really going to make things a bit easier as I go through the coco2d-x API. Looking forward to using it quiet a bit more in the near future. The capture clause of the lambda was a bit confusing at first, but luckily Microsoft's MSDN had a great article on lambdas in general and explained each part of the lambda which make it a bit clearer as to what the above code actually does. It's really no different than Lambda's in C# or Closures in Swift/Objective-C, with the exception that neither has a capture clause as far as I know.


I'm well on my way to getting my new project up and running. I've posted a couple forum threads tonight here at gamedev.net in regards to animation and bone/joint wire-up. That will probably be one of the first things I have to tackle with this game.

I plan on talking a bit more about what I'm going to be working on in another post, one where I can spend some time writing it when it's not midnight. I really want to talk through my design and development process here in this journal.

Until the next post.

Sign in to follow this  
  • Advertisement