TDWorld Development Thread

Update:

30-40 sustained FPS in our stress test!

Still not 60, but closer!

I realized there was no reason to calculate what is visible each frame. So now, I do that on a separate thread. Additionally, my render loop consisted of a tree structure, since the world is made up of tiles->quads->segments->renderables. We had to iterate through this tree of classes during each frame, probably causing lots of cache misses.

Instead now, that separate thread iterates the tree structure and populates an array of items to render. The render loop just polls from this list, meaning that rendering simply is just iterating through one array.

This is managed by having two buffers of renderables - one used for rendering, while the other is prepared for the next frame.

EDIT: 60FPS now!

I disabled blending on the avatars, which I don’t need, and also made the avatar resolution lower depending on the screen size.

2 Likes

The creep bodies can now be animations, here’s a test with some random GIF I found:

I have to figure out how to make nice plasma-like animations in Blender, then we’ll have our own :slight_smile: And we can finally get around to making the tower attacks look nice too.

1 Like

Today lots of progress on the synchronization between client/server:

  • Instead of running a “similar” simulation on the client as the server (in regards to attacks), the server now sends the attacks that the client doesn’t yet know about, and the client just renders those. This improves the accuracy a lot and removes a lot of headaches for me that kept causing delays. If needed, we can always optimize it more - trying to duplicate the simulation is not worth the headache. This also saves a lot of CPU time / battery life on the client.
  • Creep positions are still “simulated” separately however, but this is much easier than dealing with that and attacks.
  • Fixed bug where eventually all the creeps on the screen would go away. This regression was introduced by our optimization to only render creeps on screen. Inside the CreepRenderable.render() method - we set a flag - “renderedLastPosition” to true. If this was false, we wouldn’t calculate the next position. This obviously no longer works with the new system - and without it the bug is fixed.
  • A new debug system was built so that on the client I can see which creeps are being rendered that are no longer on the server, and also I can tell the client to inform the server about how many creeps it knows about. This way, on the server I can randomly select clients to see how accurate the synchronization is, if need be, to detect bugs.

Upcoming:

  • Previously, creeps came from an object pool on the client. I removed that optimization while fixing the above. I need to add that back.
  • Remove scrolling from the sign up flow - right now the sign up UI is terrible - can’t launch until that is fixed.
  • Need to find someone to make animations for the creeps/lasers.
  • Ability for creeps to attack towers! :slight_smile:
1 Like

Much progress was made on attacks, server side and client side. This adds the ability to attack another tower using creeps. There’s still much more to do, as rendering the attacks is not yet complete, however everything looks to be working server side, the client gets the events, the creeps attacking the tower on the server will eventually destroy it, award the attacker, and stop the attack/spawning attack creeps.

On the client an issue with segments/quads flickering when they pop in/out of view was fixed by adding a delay:

public boolean isInViewDelayed(Frustum frustum, long now) {
    boolean visible = frustum.boundsInFrustum(topLeft.x, 0, topLeft.y, halfWidth, 0f, halfWidth);
    if (visible) {
        lastTimeNotVisible = 0;
        return true;
    }

    if (lastTimeNotVisible > 0) {
        if (now - lastTimeNotVisible > OUT_OF_VIEW_DELAY) {
            lastTimeNotVisible = now;
            return false;
        }
    }

    lastTimeNotVisible = now;

    return true;
}

I use the same technique for particle systems (which can be relatively expensive).

1 Like

Nice. We use this for the whole game world (it’s a tree).

Some progress today regarding attacking towers with creeps:

  • When the target is destroyed, the server properly stops spawning new creeps for that attack.
  • When creating a tower, you can choose the tower type in the UI now.
  • After initiating an attack, the map automatically deselects the attacking tower.
  • Fixed broken optimization preventing some information getting sent from server to client
  • When a tower is destroyed, ensure it actually gets removed from the clients +
  • When an attack completes, cleanup the attacking creeps.
  • For the last two items, the first pass of a new concept of “event” was implemented so the server can tell clients that certain things were deleted without having to send attacks that do the equivalent damage, for example.

Also, you can now see who you are logged in as in the game menu… :slight_smile:

Progress today:

The registration flow is now paginated, instead of just being a giant form, which is terrible to fill out.

A simple paginated UI framework was created on top of scene2d:

RequestCreateUser requestCreateUser = new RequestCreateUser();

root = new PaginatedStage(isHighRes, stage, new PaginatedView[]{
        new RegistrationUserNameView(isHighRes, requestCreateUser),
        new RegistrationFactionView(isHighRes, requestCreateUser),
        new RegistrationAvatarView(isHighRes, colorPicker, requestCreateUser),
        new RegistrationEmailView(isHighRes, requestCreateUser),
        new RegistrationPasswordView(isHighRes, requestCreateUser),
        new RegistrationSubmitView(isHighRes, api, requestCreateUser),
        new RegistrationWelcomeView(isHighRes, requestCreateUser),
}, hud::closeRegistrationUI);

This way I can easily maintain and reorder parts of the registration flow, as desired. It also supports things like interstitial views, which allows certain steps of the registration flow to own all logic in that step, like finalizing creating the user (submit view) without the user having to hit submit a second time.

public interface PaginatedView extends View {
    void setPaginationView(HorizontalGroup horizontalGroup);
    void validateAndShowErrors(PaginatedViewValidationObserver observer);
    default boolean isInterstitialView() { return false; };
    default void render() {};
}

Now that it’s functional, I’m working on making it look nicer and then I’ll put up a quick video of it.

2 Likes

Today was as busy day, so all that was done was:

  • Now able to sign up as Rogue, rather than being a specific faction.
  • The server properly handles conflicts between the different factions and Rogue users.
  • Validation server side for signup for the new faction, and some refactoring.
  • UI changes, and rendering the user’s faction in the game menu.

Next I’m planning on building a simple notification system, so that when the user creates links etc, they get little messages saying that the attack/trade route they setup is working.

Also, I am still looking for an artist/3d designer.

2 Likes

Notification system created!

tdworld-notifs

It created some fun challenges to solve.

Server-side, we don’t want to create contention/load on the producers that want to create notifications. This means, for example, we don’t want to handle the fanout when we actually create the notification itself.

Instead, we fire off the notifications right away into a queue. Currently, since the server runs on one big machine it gets put in a ConcurrentLinkedQueue.

A separate thread periodically goes through and 1. Pops items out of the queue and assigns them to a user and 2. Cleans up old notifications assigned to a user that were never consumed.

The client-side event loop on the server (which runs on multiple threads) periodically polls notifications for that particular user and pushes them to the client.

This gives us clear separation of responsibilities, and if we need to ever scale horizontally, this architecture lets us.

The impl. is fairly simple:

// BEGIN MANY THREADS
public void addNotification(ID userId, Notification notification) {
    notificationsInQueue.add(new NotificationQueued(userId, notification));
}

// If the server shuts off during the call of this method, some notifications will be lost.
// It would be nice if the client could say what notifications it got, and *then* we remove them from the server.
public void getUserNotifications(ID userId, List<Notification> result) {
    Queue<Notification> notifications = notificationsByUserId.get(userId);
    while (notifications != null && !notifications.isEmpty()) {
        Notification next = notifications.poll();
        if (next != null) {
            result.add(next);
        }
    }
}
// END MANY THREADS

// BEGIN PROCESSOR THREAD
private void bucketQueueByUser() {
    while (!notificationsInQueue.isEmpty()) {
        NotificationQueued notificationQueued = notificationsInQueue.poll();
        if (notificationQueued != null) {
            Queue<Notification> notifications = notificationsByUserId.get(notificationQueued.userId);
            if (notifications == null) {
                notifications = new ConcurrentLinkedQueue<>();
                notifications.add(notificationQueued.notification);
                notificationsByUserId.put(notificationQueued.userId, notifications);
            } else {
                notifications.add(notificationQueued.notification);
            }
        }
    }
}

private void cleanupOldNotifications() {
    long now = System.currentTimeMillis();
    for (Map.Entry<ID, Queue<Notification>> entry : notificationsByUserId.entrySet()) {
        entry.getValue().removeIf((notification) -> notification.createdAt + notification.lifeDuration >= now);
    }
}

public void processQueue() {
    cleanupOldNotifications();
    bucketQueueByUser();
}
// END PROCESSOR THREAD

We also have monitoring for if things like the processor thread taking too long.

The notifications are also persisted to disk for reliability etc. If you do something, like create a very long link, and then close the app, and then we do a deployment (restart server), and come back a week later, you’ll get the notification.

On the client, the notification UI framework is created in libgdx’s regular ol’ scene2d.

We now have notifications for when towers created, links are created, creeps start spawning for new links (attacks/trade routes).

Here’s a quick demo of the result!

https://www.youtube.com/watch?v=CyCDWZgmUpc

I have some plans to expand notifications, maybe after launch, where each notification can be tied to a geographical position, so you can click “see details” on the notification and jump there.

A refactoring of how the towers are linked has been done, which allowed me to resolve a bug where if a tower is attacked and destroyed, the routes to/from that tower on the client did not go away.

This also makes working with data in the backend much easier, as instead of storing to/from ids on each tower, it’s a directed graph, represented as a ConcurrentHashMap of TowerLink, sharded by our existing Shard mechanism. This makes reasoning about the data much easier and more efficient.

It also means that now I can add the ability to upgrade towers to the point that you can link a tower to/from many others, for example.

The link creation API has changed, but still simple, with the link to shard propagation encapsulated in TowerLink itself:

TowerLink towerLink = new TowerLink(towerA, towerB, null);
towerLink.propagateToShards(remoteGameState.getWorld());

RouterProvider.economyRouteRouter.enqueueForTower(towerA, towerLink);
remoteGameState.addNotification(
        new Notification(NotificationType.TOWER_ECONOMY_LINK_ENQUEUED)
);
1 Like

When selecting a building or tower, it’s nice to know, in the sea of buildings, what you’ve selected. Up to now, I was using a particle based animation that spun around the selected building.

This has been replaced with an arrow.

Also, towers now support health bars, and some race conditions on the client have been fixed to ensure the attacks finish playing before the tower is actually removed (the towers get removed a little sooner on the server than on the client).

https://www.youtube.com/watch?v=Vk1qj7-MQQA

Note: In the video things like the health bars & avatars don’t rotate to face the camera and movement is janky. This is just because of “God mode” on desktop needs some work.

This was done on Sunday, I was just lazy in writing the update.

Coming up next week:

  • Creeps not going away at end of attack?
  • Store last location on disk + show “outdated location” message
  • Camera to go to building on selection
  • Encryption for login/session info in transit

Some progress today - the Neutral building selection UI:

Lots more changes to come, like making history look nicer.

I had some ideas on making the stats look fancy, but going with this for now to get things done.

Includes a major refactor to how the building selection UI system works, so I can start churning out these UIs quicker.

1 Like

Building history now functional (previous screenshot used testing data):

Also, we only keep the last 30 days of history to prevent it from exploding in size forever.

The stats and module slot count also now come from the server.

This required a refactor to how towers are created, since to create the stats we currently have to actually construct a tower, just like on tower creation, from the building feature, and run the same calculation mechanism for the base stats.

There’s still more to do here before launch:

  • Make each entry in the history look nice (also lots of missing stuff like the time the event occurred).
  • Button to “see all stats”
  • Loading animation. - EDIT: done.
  • Message when there is no history. - EDIT: done.

After these are done, I plan to move onto architecting and implementing tower upgrades.

Great job! I hope you are strong enough to at least roll out something :slight_smile: good luck!

It will be launched when it’s ready. I only work on it on Sunday mornings currently as I have other projects in production that take most of my time.

Major progress on building history, and the ability to purchase things with credits and real money in the backend.

First, building history looks a little better, with names and dates added:

Also, the cost of the building is now calculated, and shown here.

Second, a new system for managing purchases has been created in the backend. This introduces the new concepts:

  • The Store and Products (Think, being able to purchase tower modules).
  • Purchases (and purchase history).
  • Inventory

The existing “user credits” is now used in addition to the new credit system, which is based on Purchases. This means we have two concepts: Credits and Earned Credits. This is an optimization, and is transparent to the user. This is because if there is a server restart, it is okay to lose some earned credits (which the user earns, for example, from trade routes), but it is not okay to lose credits the user purchased with real money.

We may separate these systems, if I can be creative enough to come up with different names for the currency, but it seems like a source of confusion.

Third, the new system for managing Purchases is strongly consistent. Every small change is written to disk, which is different than most of how the rest of the game works where we simply checkpoint to disk very infrequently. Each user has their own small database, to help prevent potential corruption from impacting multiple users (however this corruption is unlikely as we use our own “safe write” mechanism).

Forth, when calculating the user’s credits, we look at their earned credits, and then combine them with their purchased + spent credits. Each event loop tick for the Remote Game State for that user checks if the purchases have been invalidated for that user, and if need be recalculates the credit value and updates the client. This is an optimization to prevent too many reads from disk.

There is no central place where we store the user’s purchased/spent credits. When we need a total, we calculate it from the history to maximize accuracy. This is very infrequent, and most likely not worth optimizing right now.

Fifth, the tower creation mechanism now calculates the cost of the building based on its volume and double checks if the user can claim that tower. If they can, it creates a purchase event and stores it. This is the first usage of the purchase system.

Upcoming:

  • Store, ability to purchase tower modules with credits, ability to apply them to towers.

Yesterday progress was made on the store. I now have a system to manipulate the store items in a spreadsheet and export that sheet into a format the backend understands.

A very, very early version of the product list UI component was made:

Still need a designer to make icons and such. These are placeholders.

1 Like

Today progress was made on the store product list - the item tile backgrounds are now generated at runtime to allow for easier flexibility in the future and for generating animations.

The backgrounds of the items in the store now have a slight “glow” animation that goes row-by-row.

The displayed credits in the top right of the HUD was also updated to actually have an icon.

In progress:

  • Product details modal
  • Tabbing between store and inventory (custom common TabbedView component done)
  • Ability to purchase modules with in-game credits and apply to towers
2 Likes

Thanks for all the love @CommanderKeith! :slight_smile:

1 Like