Client/Server Topology

Before I jump into the details of our architecture, I'd like to point out that we haven't had an update in a little over a month. Although there doesn't seem to be much activity, we have been still been making a lot of progress. Multiple updates are being added to the upcoming (0.2.1) version every week.

This will likely be our largest release thus far, which already includes features such as Player to Player trading, ability to interact with a NPC and the ability to receive rewards from completing a quest. Frequent updates to the site will return after the winter holidays are over.

MMORPG Architecture

Given that we're using Unity for our player client, I've had a few folks ask how we're handling the server-side for our game. Is it in Unity? And if not, how are we handling positioning and collision detection?

The answer... it's a combination of both for now.

Lets go through each component and describe their role.

Unity Player Client

This should be self explanatory. This is the Unity client that players will use to play the game.

MMORPG Login Server

The Login Server primarily exists to authenticate the user and get them to the correct World Server. This is the point of entry for the Unity client. First, a player must go through the Login Server to choose a World to play on.

  • Handle registration of new accounts
  • Handle authentication
  • World servers can register themselves with the login server
  • Provides list of worlds to players who have successfully authenticated

MMORPG World Server

The World Server orchestrates the Zone Servers as well as contains all of the logic and processes that must exist outside of a particular zone. A few of those processes:

  • Character creation
  • Player to player, group, and guild chat channels
  • Party management
  • Guild management
  • Routing players between Zone Servers
  • Spawning of Zone Servers

MMORPG Zone Servers

Given the complexities with running a seamless world, we instead designed our zones that they each are ran by a single Zone Server. We also wanted NPCs to chase players until they have left a zone, rather than simply giving up and turning around after a few hundred feet. This behavior was simple enough to support by having strict boundaries between zones.

Most of the game logic lives on the Zone Server:

  • Combat
  • Trading
  • Merchants
  • Spawn management
  • Inventory Management
  • Aggro management
  • Say, Out of Character, Shout chat channels

I haven't said it explicitly yet, but the Login, World, and Zone servers are all C# console applications. They are not Unity servers, and at this time they are not aware of the physical layout of the world itself. Instead, we introduce an authoritative Unity client.

Authoritative Zone Client

The Authoritative clients are our current solution to not having the terrain available on the Zone Servers. Eventually we may find that it will be too hard to scale up to 40-50 zone clients on a single machine, but without much optimization we have been able to handle the Login, World, and multiple Zone Servers/Clients on a relatively outdated server.

So what does this client handle exactly? Right now the primary role is to handle/drive NPC movement/behavior. Eventually we will try to move line of sight checks behind this authoritative client, however, if such a change introduces too much latency we will need to consider exporting our terrain meshes into a form that our actual Zone Servers can interpret, or live without having authoritative checks for certain behaviors. Our hope is that the latency will suffice by keeping this client on the same physical machine as the server.

I will note that not all of the NPC behavior lives on this client. For example, the aggro list for a NPC is driven by the Zone Server. The authoritative client will call the Zone Server and tell it when it has proximity aggro to a player, however, it is up to the Zone Server to identify whether or not the NPC should change targets. What if another player had been building aggro on the NPC via combat/spells for a while? The Zone Server will be aware.

Other NPC behaviors strictly live on the authoritative client. For example, when a NPC is spawned the authoritative client will fetch the character's waypoint definition. The waypoints will be used to move the NPC until it has aggro, at which point the Zone Server begins to drive the NPC's targets.

Funkhouse Ecosystem

This year we've slowly started to break out of the MMORPG specific code and into other services that will be needed eventually.

For context, right now all of the Items, Spells, Npcs, etc. are all defined in the World Server. They're simply database entries that the World and Zone servers can access, however, that means we have to ship a lot of metadata to the client each and every time they start playing.

Our plan has always been to build a process where certain entity definitions (Items, Spells, Npcs, etc.) are created in one location and then included in the client and servers locally at build time. However, with how large we plan to make this MMORPG and with the lack of developers we have working on this project, we have been wanting to design a process for non developers to submit new content.

With the recent focus on questing, the building up of a town with 40-50+ NPCs, we realized that the time to start building this process is now. That way we can have other contributors start submitting items, changes to items, etc. all from a simple to use web application while we finish up the last set of features for our initial milestone.

With that, lets take a few feet back and see how we're expanding to support the above idea.

Funkhouse SSO Service

This service was put in place to start handling the different types of identities that can exist between our games/services. Alternatively, we could have kept the Content Submission Service completely separate, but rather than building multiple identity, RBAC type of systems to control who can submit vs approve content, or even identify a Game Master/Administrator, we decided to centralize the identity between all of our services.

Now, the Login Server can handle registration/authentication by itself for local development, but when deployed to our Public Test Server it will rely entirely upon the SSO service. At that point, the Login Server becomes more of a token exchange point as well as a way to lookup/connect to a world.

The SSO service is actually pretty simple, though I may be biased as I worked on an Authentication system for a couple years. Right now it has the ability to create identities for users/services, allow administrator services to manage a user's role (MMORPG player VS Content Submitter), generate tokens for authenticated users/services, and authorize a user/service's access based on its assigned roles.

Content Submission Service

For the most part, this service wraps our MMORPG database entities with a few additional fields: created at, description, updated by, etc. Those entities are then exposed via a REST API which will be consumed by our upcoming content submission web application. Additionally, whenever a change is submitted, a new Submission Change Entry will be created which has to then be approved before the change will be exported at build time.

This service doesn't really handle much more than that. We've built the wrapper to be generic enough so that you're essentially managing the database entities using a submission-based API.

Below is an example of our abstract Submission DAO, which generically handles the updating/fetching of a entity. Note that we're currently using PetaPoco, a light ORM for C#.

public abstract class AbstractSubmissionDao<T> : AbstractDao<T> where T : IContentSubmissionModel {
  private readonly SubmissionDao _submissionDao;

  public abstract ContentSubmissionType ContentType { get; }

  protected AbstractSubmissionDao(IDatabaseSessionFactory dbSessionFactory, IDataTypeFactory dataTypeFactory, SubmissionDao submissionDao, IFunkyLogger logger)
    : base(dbSessionFactory, dataTypeFactory, logger) {
      _submissionDao = submissionDao;
    }

  private void InsertSubmission(T content, int createdById) {
    content.Submission.Type = ContentType;
    _submissionDao.InsertSubmission<T>(content.Submission, content, createdById);
    content.SubmissionId = content.Submission.Id;
  }

  public void Insert(T entity, int createdById) {
    InsertSubmission(entity, createdById);
    Insert(entity);
  }

  public IEnumerable<ContentSubmissionListModel<T>> GetListModel() {
    var sql = $"SELECT * FROM {TableName} c " +
      $"INNER JOIN {_submissionDao.TableName} s on s.Id = c.SubmissionId " +
      $"INNER JOIN submission_change_entries e on e.Id = (SELECT Id FROM submission_change_entries WHERE c.SubmissionId = SubmissionId GROUP BY Id, SubmissionId ORDER BY UpdatedAtUtc ASC LIMIT 1) " +
      $"INNER JOIN submission_change_entries e2 on e2.Id = (SELECT Id FROM submission_change_entries WHERE c.SubmissionId = SubmissionId GROUP BY Id, SubmissionId ORDER BY UpdatedAtUtc DESC LIMIT 1);";
    var results = Db.Fetch<T, SubmissionEntity, SubmissionChangeEntryEntity, SubmissionChangeEntryEntity, ContentSubmissionListModel<T>>(
      (p, s, e1, e2) => {
        p.Submission = s;
        return new ContentSubmissionListModel<T>(p, e1.UpdatedById, e2.UpdatedById, e1.UpdatedAtUtc, e2.UpdatedAtUtc);
      }, sql);
    return results;
  }

  public ContentSubmissionDetailModel<T> GetDetailModel(long id) {
    var sql = $"SELECT * FROM {TableName} c " +
      $"INNER JOIN {_submissionDao.TableName} s on s.Id = c.SubmissionId " +
      $"INNER JOIN submission_change_entries e on e.Id = (SELECT Id FROM submission_change_entries WHERE c.SubmissionId = SubmissionId GROUP BY Id, SubmissionId ORDER BY UpdatedAtUtc ASC LIMIT 1) " +
      $"INNER JOIN submission_change_entries e2 on e2.Id = (SELECT Id FROM submission_change_entries WHERE c.SubmissionId = SubmissionId GROUP BY Id, SubmissionId ORDER BY UpdatedAtUtc DESC LIMIT 1) " +
      $"WHERE c.Id=@0;";
    var results = Db.Fetch<T, SubmissionEntity, SubmissionChangeEntryEntity, SubmissionChangeEntryEntity, ContentSubmissionDetailModel<T>>(
      (p, s, e1, e2) => {
        p.Submission = s;
        return new ContentSubmissionDetailModel<T>(p, e1.UpdatedById, e2.UpdatedById, e1.UpdatedAtUtc, e2.UpdatedAtUtc);
      }, sql, id);

    if (results.Count == 0) {
      throw new Exception("No results found.");
    } else if (results.Count > 1) {
      throw new Exception("More than 1 result found");
    }
    return results[0];
  }

  public void Update(long contentId, T entity, int updatedById) {
    var existingEntry = ById(contentId);
    if (existingEntry == null) {
      throw new Exception("Entry does not exist");
    }
    _submissionDao.UpdateSubmission<T>(existingEntry.SubmissionId, entity.Submission, entity, updatedById);
    entity.SubmissionId = existingEntry.SubmissionId;
    Update(entity);
  }
}

Then, we have a generic controller to handle the CRUD updates.

public class ContentSubmissionController<T> : ApiController where T : IContentSubmissionModel {
  private readonly AbstractSubmissionDao<T> _dao;
  public ContentSubmissionController(AbstractSubmissionDao<T> dao) {
    _dao = dao;
  }

  public IEnumerable<ContentSubmissionListModel<T>> Get() {
    // TODO: Implement pagination
    return _dao.GetListModel();
  }

  public ContentSubmissionDetailModel<T> Get(int id) {
    return _dao.GetDetailModel(id);
  }

  public void Post([FromBody]UpdateSubmissionModel<T> value) {
    _dao.Insert(value.Content, value.UpdatedById);
  }

  public void Put(int id, [FromBody]UpdateSubmissionModel<T> value) {
    _dao.Update(id, value.Content, value.UpdatedById);
  }

  public void Delete(int id) {
    _dao.Delete(_dao.ById(id));
  }
}

As you'll see by the TODO.. there is still a bit of polishing we need to do to for this service to actually be complete:

  • Implement pagination when fetching a list of records
  • Add role restrictions to endpoints
  • Authenticate roles with Funkhouse SSO service
  • APIs to approve a submission change

Those are some of the more important items, but we'll likely add a few more things here and there as we wrap up the web application.

Whats Next

After the content submission web application / service are complete we will return to focusing on the "Initial Version" as defined by our roadmap.

There is really not a lot much left to finish the initial version, albeit we will probably spend a month or so handling some left over //TODOs on our clients and game servers. NPC behavior interactions are pretty much complete, outside of setting a player's global quest flag. In fact, we had just finished the ability to get experience/items from a NPC after turning in a required set of items/coins when we decided to break off onto content submission.

Once the above is complete we will be putting most of our time into expanding our content to a place where we can perform a larger scale test with 10-15 players. This includes a town, a dungeon, and a large enough set of items, quests, spells, npcs, factions, etc.

What did you think about this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Be the first to comment

Leave a comment

Your email address will not be published.