Jump to content
Site Stability Read more... ×
  • Advertisement
Sign in to follow this  
  • entries
  • comments
  • views

About this blog

Game Server Development

Entries in this blog


Slick: Persistence with Style

[font='times new roman']What is Slick?[/font]
[font='times new roman'] Slick is a Functional Relational Mapper (FRM as oppose to the commonly used ORM) that allows us to compose type-safe database queries. Slick also allows us to interact with our database in the same way we would with a Scala collection; so no more raw SQL.[/font] [font='times new roman'] What's most important though, is that execution of our queries on our database is done asynchronously. This makes Slick a perfect fit for our Akka server.[/font] [font='times new roman'] Does Slick play well with PostgreSQL?[/font]
[font='times new roman']Yes. Slick supports a multitude of relational database management systems; but I prefer to work with PostgreSQL because it's open-source.[/font] [font='times new roman'] Slick in action[/font]
[font='times new roman']In my previous journal entry we saw our LoginHandler actor handle incoming messages and use a UserDAO object to do look ups and process user registrations. Let's take a closer look at our UserDAO class.[/font][code=:0]package com.vinctus.venatus.daoimport scala.concurrent.{Future, Await}import scala.util.{Try, Success, Failure}import scala.concurrent.ExecutionContext.Implicits.globalimport java.util.UUIDimport java.sql.Timestampimport org.mindrot.jbcrypt._import slick.driver.PostgresDriver.api._import com.vinctus.venatus.models.Userclass UserDAO extends DB { private val Users = TableQuery[UsersTable] def list: Future[Seq[User]] = db.run(Users.result) def find(id: UUID): Future[Option[User]] = { db.run(Users.filter(_.id === id).result.headOption) } def find(email: String): Future[Option[User]] = { db.run(Users.filter(_.email === email).result.headOption) } def create(user: User): Future[Option[User]] = { val pwHash = BCrypt.hashpw(user.password, BCrypt.gensalt()) db.run((Users += user.patch(password = Some(pwHash))).asTry).flatMap({ case Success(result) => find(user.id) case Failure(e) => Future(None) }) } def update(user: User): Future[Option[User]] = { db.run(Users.filter(_.id === user.id) .map(u => (u.email, u.firstName, u.lastName)) .update(user.email, user.firstName, user.lastName).asTry) .flatMap(_ match { case Success(result) if result == 1 => find(user.id) case Failure(e) => Future(None) }) } def delete(id: UUID): Future[Int] = db.run(Users.filter(_.id === id).delete) private class UsersTable(tag: Tag) extends Table[User](tag, "users") { def id = column[UUID]("id") def email = column[String]("email", O.PrimaryKey) def password = column[String]("password") def firstName = column[Option[String]]("first_name") def lastName = column[Option[String]]("last_name") def createdAt = column[Timestamp]("created_at") def updatedAt = column[Timestamp]("updated_at") def idIdx = index("users_id_index", id, unique = true) def emailIdx = index("users_email_index", email, unique = true) def * = (id, email, password, firstName, lastName, createdAt, updatedAt) ((User.apply _).tupled, User.unapply _) }}
[font='times new roman']Here we can see the find by email method I was using in LoginHandler to provide a user for user authentication.[/font][code=:0]def find(email: String): Future[Option[User]] = { db.run(Users.filter(_.email === email).result.headOption)}
[font='times new roman']This method is non-blocking and promises a Future[Option[User]]. Our User is a Scala case class and looks like:[/font][code=:0]package com.vinctus.venatus.modelsimport java.util.UUIDimport java.sql.Timestampimport java.util.Datecase class User( id: UUID = UUID.randomUUID, email: String, password: String, firstName: Option[String] = None, lastName: Option[String] = None, createdAt: Timestamp = new Timestamp(new Date().getTime), updatedAt: Timestamp = new Timestamp(new Date().getTime)) { def patch( email: Option[String] = None, firstName: Option[String] = None, lastName: Option[String] = None, password: Option[String] = None): User = this.copy( email = email.getOrElse(this.email), password = password.getOrElse(this.password), firstName = if (firstName.isDefined) firstName else this.firstName, lastName = if (lastName.isDefined) lastName else this.lastName, updatedAt = new Timestamp(new Date().getTime))}
[font=tahoma][font='times new roman'] Conclusion[/font][/font]
[font='times new roman']We now have a basic skeleton for processing messages from the client that can persist data to the database when needed. From this point on we'll be adding more logic to handle more game features and also introduce Actors, DAOs, and domain models where needed to flesh out the full game server.[/font]




Akka IO and Protocol Buffers

[font='times new roman']Fun with Akka[/font]
[font='times new roman'] Work on the game server has proven to be a lot of fun using Akka, and even more so writing it in Scala. Let's take a closer look to what's been done so far.[/font] [font='times new roman'] I've decided that I'll have a separate login server that will handle user login and registration, and then a game server that will handle logged in users.[/font] [font='times new roman'] The login server is an Akka Actor that listens for connections on a given port. When a connection is established, the login server passes the connection to a newly created Actor which handles the messages over the connection. This frees up the login server to handle new connections immediately and will continue spawning connection handlers when more connections are made.[/font] [font='times new roman'] Here's a look inside LoginServer.scala which implements the process I've described above:[/font]package com.vinctus.venatus.serversimport akka.actor.{ Actor, Props }import akka.io.{ IO, Tcp }import java.net.InetSocketAddressimport com.vinctus.venatus.handlers.LoginHandlerclass LoginServer extends Actor { import Tcp._ import context.system IO(Tcp) ! Tcp.Bind(self, new InetSocketAddress("localhost", 6666)) def receive = { case Tcp.CommandFailed(_: Bind) => context.stop(self) case c @ Tcp.Connected(remote, local) => val handler = context.actorOf(Props[LoginHandler]) val connection = sender connection ! Tcp.Register(handler) }}
[font='times new roman']Getting a Handle on Protobuf[/font]
[font='times new roman']The login handler is expecting messages that are encoded using Google's protocol buffers. Currently there are two types of messages it can handle; a login message, and a register message.[/font]
[font='times new roman'] Inside my .proto file, I have the following schema for my messages:[/font]message WrapperMessage { oneof msg { Login login = 1; Register register = 2; }}message Login { required string email = 1; required string password = 2;}message Register { required string email = 1; required string password = 2; optional string firstName = 3; optional string lastName = 4;}
[font='times new roman']Because protobuf messages aren't self describing, I'm using the technique of having a parent message (in this case the 'WrapperMessage') with a 'oneof' field that holds the message I'll be matching against later. This way I first decode the expected 'WrapperMessage', and match against the type of inner message, and use the relevant decoder for that message.[/font]
[font='times new roman']Here's the 'LoginHandler' implementing the technique I'm describing above:[/font]import scala.concurrent.ExecutionContext.Implicits.globalimport akka.actor.{ Actor, ActorRef, Props }import akka.io.{ IO, Tcp }import akka.util.ByteStringimport akka.io.Tcp.{ Write, Received }import org.mindrot.jbcrypt._import com.vinctus.venatus.protobuf.LoginProtos._import com.vinctus.venatus.protobuf.LoginProtos.WrapperMessage.Msgimport com.vinctus.venatus.models.Userimport com.vinctus.venatus.dao.UserDAOclass LoginHandler extends Actor { val userDAO = new UserDAO def receive: Receive = { case Tcp.Received(data) => val wrapperMessage = WrapperMessage.parseFrom(data.toArray) val loginResponse = LoginResponse() wrapperMessage.msg match { case Msg.Login(l) => userDAO.find(l.email).map(_ match { case Some(user) => if (BCrypt.checkpw(l.password, user.password)) { sender ! Tcp.Write(ByteString(loginResponse .withUserId(user.id) .withSuccess("Login successful.").toByteArray)) } else sender ! Tcp.Write(ByteString(loginResponse .withError("Invalid password for that user.").toByteArray)) case None => sender ! Tcp.Write(ByteString(loginResponse .withError("No user exists with that email address.").toByteArray)) }) case Msg.Register(r) => val user = new User( email = r.email, password = r.password, firstName = r.firstName, lastName = r.lastName) userDAO.create(user).map(_ match { case Some(user) => sender ! Tcp.Write(ByteString(loginResponse .withUserId(user.id) .withSuccess("Registration successful.").toByteArray)) case None => sender ! Tcp.Write(ByteString(loginResponse .withError("Failed to register.").toByteArray)) }) case Msg.Empty => // TODO } case _: Tcp.ConnectionClosed => context.stop(self) }}
[font='times new roman']We now have an Akka server capable of handling protobuf messages.[/font]




Beginnings of a Game Server

[font='times new roman']About[/font]
[font='times new roman'] This journal intends to follow the development progress of a MORPG game server called Venatus. The game server will be mostly written in Scala (more about the language can be found here). The codebase will [/font][font='times new roman']be open-source and contributions made by volunteers will be welcome. The repository for the source is locate[/font][font='times new roman']d at https://github.com/emaxedon/vinctus-venatus.[/font] [font='times new roman']Game Mechanics[/font]
[font='times new roman'] The game mechanics will follow that of a basic fantasy multiplayer online RPG. The game server will not handle logic for any new or innovative forms of gameplay. Only the basic features of common MORPGs will be implemented. These features include but are not limited to:[/font]
[font='times new roman']User registration and authentication[/font]
[font='times new roman']Character creation (picking class, gender, name)[/font]
[font='times new roman']Chat system (global chat, map chat, pm)[/font]
[font='times new roman']NPCs[/font]
[font='times new roman']Quest system[/font]
[font='times new roman']Player level and attributes (players level up [/font][font='times new roman']by gaining experience completing quests, and by killing non-player entities)[/font]
[font='times new roman']Player inventory and equipment[/font]
[font='times new roman']Player movement, spawning, class skill actions[/font]
[font='times new roman']Player to player trading[/font]
[font='times new roman']Player friend system[/font]
[font='times new roman']Guild system[/font]

[font='times new roman']Dependencies [/font]
[font='times new roman'] The game server will be designed to be non-blocking, scalable, and resilient. To accomplish the goal of the game server, the following tools will be used:[/font]
[font='times new roman'] Akka - a toolkit for developing asynchronous, non-blocking message-driven applications that run on the JVM[/font]
[font='times new roman'] Google's Protocol Buffers - a language-neutral, platform-neutral mechanism for serializing structured data[/font]
[font='times new roman'] ScalaPB - a protocol buffer compiler plugin for Scala. It will generate Scala case classes, parsers, and serializers[/font]
[font='times new roman'] Slick - a functional relational mapper that allows for type safe database queries written in Scala[/font]
[font='times new roman'] PostgreSQL - open-source relational database management system[/font]
[font='times new roman'] jBCrypt - Java implementation of OpenBSD's Blowfish password hashing[/font]
[font='times new roman']More to come...[/font]

[font='times new roman']Game Client[/font]
[font='times new roman']The game client will be a low graphics application using the Unity3D game engine. The client will be developed concurrently for testing purposes only. Development on the client will not be the focus of attention for this journal, but its codebase will be open-source.[/font] [font='times new roman']License[/font]
[font='times new roman'][color=rgb(51,51,51)]Vinctus Venatus is distributed under the MIT License, meaning that you are free to use it in your free or proprietary software.[/color][/font]



Sign in to follow this  
  • Advertisement

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!