Welcome, Guest. Please login or register.

Login with username, password and session length

 
Advanced search

1411421 Posts in 69363 Topics- by 58416 Members - Latest Member: timothy feriandy

April 18, 2024, 03:23:57 AM

Need hosting? Check out Digital Ocean
(more details in this thread)
TIGSource ForumsCommunityDevLogsDECEIVER
Pages: 1 ... 7 8 [9] 10
Print
Author Topic: DECEIVER  (Read 94848 times)
etodd
Level 3
***



View Profile WWW
« Reply #160 on: December 22, 2017, 07:08:44 PM »

Working on some scenes for the trailer. Here's Seven and Samsa in "holographic cyberpunk space command center" mode:



He folds up into cyberdoggo form:



And Seven packs up to leave:



The backpack animation was incredibly tricky. It's not perfect, but I'm okay with it. I did it by duplicating the bones that affect the backpack, then creating constraints that glue the new bones to the original ones. To put the backpack on, I just animate the influence of these constraints from zero to one.

Logged

etodd
Level 3
***



View Profile WWW
« Reply #161 on: December 30, 2017, 09:46:53 AM »

Still working on the trailer. Here's a new shot from it:



It's coming together quicker than anticipated. Most of the footage is pretty close to final, and now it's just a matter of getting the audio together.

In the mean time, I realized while watching gameplay footage that the weapons were in desperate need of camera recoil. So I added some.



It's just velocity and acceleration. I overwrite the velocity when the gun fires, then the acceleration (or "gravity") brings it back down. Once the angle gets close to zero, I limit the velocity by a multiple of the current angle, so it slows down instead of slamming back to zero. Relevant code here.

Unfortunately this new feature means I now have to re-shoot several scenes in the trailer. Tongue
Logged

The Armorman
Level 2
**



View Profile
« Reply #162 on: December 30, 2017, 11:10:23 AM »

cyberdog looks cool
Logged

BELOW FOR GOGNIOS

ABOVE, FOR GOGNIOS
etodd
Level 3
***



View Profile WWW
« Reply #163 on: January 06, 2018, 08:06:38 AM »

I got an opportunity to talk to Michael Cox, marketing guy from Crows Crows Crows. He's @DevMicco on Twitter, he has DMs open. Great guy with super helpful advice.

He told me to release the trailer, release the demo, push PR, everything, but don't launch Kickstarter. Instead funnel people into Discord and mailing lists. then launch Kickstarter when there's enough people to make it succeed on the first day.

That makes a lot of sense to me, so that's tentatively the plan I'm going with now.

In the meantime, we had another playtest session on Thursday and I came away with a bunch of balance tweaks and bug fixes, which I was able to knock out yesterday.

The most interesting news is, the game now has a Discord bot.



This thing allows you to indicate when you are available to play, and if anyone else is online or marked available during that time, you'll get a notification. It also periodically blasts out statistics about who's playing.

I've been using vyte.in to organize playtest sessions, and it hasn't been great, so hopefully this will work better.
Logged

etodd
Level 3
***



View Profile WWW
« Reply #164 on: January 10, 2018, 08:50:20 AM »

Initial Discord integration is done. You can now see what people are doing in-game.



And if you're in a game, you can invite a person or a whole channel to play with you, even if you're in a private server.



The invite updates the number of players in the server in real-time.

I also upgraded the bot to place LFG players in a special role and mention them specifically when people are playing.



I'm really impressed by the quality of Discord's software. Their APIs are well-designed. Everything they do is a pleasure to work with.
Logged

etodd
Level 3
***



View Profile WWW
« Reply #165 on: January 16, 2018, 09:05:32 AM »

New map WIP

I decided the maximum number of players I want to support is 12, so the game can support 4 teams of 3 or 3 teams of 4. In light of that, I started working on a new map with enough room for so many players. This will be the largest map yet.







It's still a work in progress, but sometimes I'm tempted to leave parts purposely unfinished.

Presets
Some friends were kind enough to play a few local matches of Deceiver with me, and even though most of them were familiar with shooters, they were still confused. It didn't help that the game was in the middle of some of the design overhaul changes described below.

What I should have done was set the game to unlock all the abilities and disable the advanced stuff so we could all just shoot each other. The game does offer those settings, but I didn't want to sit around tweaking settings while they waited.

This is related to another problem I have with the servers online. Playtesters have created 77 different servers with different settings. It's impossible to see at a glance what the rules are for each server. Also, when creating a server, it's overwhelming to see all these options, and it encourages you to tweak all of them, which is usually not a great idea.

Presets are the solution I came up with. When you create a server, you can choose "Standard", "Arcade", or "Custom". Now you can see at a glance what to expect from a server. Arcade unlocks all abilities, and Custom lets you go crazy with any setting you want. The nice part is, these presets work with all three game modes. I can easily envision more presets like "Snipers Only", "No Shields", "Fast Cooldowns", etc.



Hardware cursor

Ever played a game where the cursor felt laggy and unresponsive? That was my game until just recently. Turns out, computers use a separate low-latency hardware path to render the cursor. I switched from my custom OpenGL cursor to a hardware cursor. Unfortunately, that meant my vector-based anti-aliased cursor mesh wouldn't work anymore.

I spent WAY too much time trying to pixel art a cool looking cursor, and ended up settling for a plain ol' crosshair. I may switch to the default system cursor, but I think this helps everything fit together better.



Design overhaul

I made a number of sweeping design changes. Below is a list, with the accompanying reasoning behind each change.

  • Remove ticket system from Assault. Assault is an attack/defend mode, and previously the defenders won by either running the clock out or exhausting the attackers' tickets (respawns). Now the ticket limit is gone. Reasoning: Originally, both teams actually had limited tickets, which made the game type devolve into just regular ol' team deathmatch. With only the attackers limited by tickets, it still meant the defenders were basically playing deathmatch. Receiving energy and deterring the enemy is enough of a reward for killing an enemy player, there's no need to attach a win condition to it. Also, the ticket system had to scale based on the number of players, so if a player joined or left mid-match, the number of tickets would change. Confusing UX.
  • Remove spawn selection. Previously you could spawn from any battery you captured, and by default you spawned at the battery nearest to where you last died. This is all gone now. You only have one spawn point. Reasoning: it's important to have downtime with lower intensity gameplay. Also, the decision of where to spawn was never interesting, and mostly just served to confuse newer players.
  • Make turrets auto-heal and decrease player damage against them. Reasoning: Once again, over the past year or so I forgot that the Assault game mode is basically a MOBA. Players are not supposed to fight turrets themselves. It's not interesting or fun. So now, it's mostly worthless to fight turrets.
  • Make ability re-purchases cost nothing. Once you've purchased an ability, you can now replace it with another one and later switch back to it free of charge. Previously you had to purchase it all over again. Reasoning: I want to encourage more diverse usage of different abilities. Also it feels more friendly to the player.
  • Make all abilities free to use. Previously, you would pay energy to buy an ability, and then some abilities also required energy to use. Now, energy is only used to buy abilities. Reasoning: it was always impossible to balance prices for moment-to-moment combat purchases against prices for longer time-frame purchases. If a high level ability costs 1000 energy to unlock, and a low-level ability costs 20 energy to use, that means you can spam the low-level ability 50 times. Also, you never got a sense of progression because you're always spending your progression currency on combat. The number never really goes up. Now the abilities are easy to balance: I just put separate cooldowns on them. And your progression is much easier to see, since your energy always goes up until you spend it on an ability.
  • Change the minion ability to a passive spawn rate boost. Previously you could use it to spawn minions at will. Now it just takes up an ability slot and increases the rate at which minions spawn from your captured batteries. Reasoning: the minion ability previously required no strategy. You could just show up at an enemy base and plop down 10 minions in as many seconds. Now you are more encouraged to capture and protect batteries, since they spawn minions.
  • Change DM and CTF modes to no longer spawn minions by default. Reasoning: the DM and CTF modes, which should be straightforward for anyone familiar with shooters, confused my friends who are familiar with shooters. These are the modes I use to introduce the game to new players, since there's much less to explain. But people are still confused, and one of the major confusion points is "who are all these people walking around?" It just makes the game feel unfocused and chaotic. I still think minions are a blast, and people love shooting them, so I'm keeping the minion boost ability in for these game modes. Instead of boosting the minion spawn rate, it enables them spawning at all.

Stealthed rectifiers
Rectifiers do two things: they heal stuff, and they create a field which provides stealth for your drone and alerts you to the presence of enemy drones.

Previously, rectifiers were pretty ineffective because they were easily destroyed. Now they're tougher because they're stealthed by default, so enemies can't see them unless they plop down a rectifier of their own.

Unfortunately, the healing particle effect I was using completely gave away the rectifier's position. It showed a trail of particles going from the rectifier to the object being healed. I changed the effect to use a sphere mesh instead. Had to modify the instancing system to allow per-instance colors.



Suicide minions

You can now launch a grenade at a friendly minion and it will attach to the minion's back, waiting for a hapless victim to wander near. While implementing this feature, I accidentally turned the minions into giant walking grenades:

Logged

nathy after dark
Level 8
***


Open Sourceress


View Profile WWW
« Reply #166 on: January 16, 2018, 10:14:21 AM »

Gonna have to follow this one!
Logged

io3 creations
Level 10
*****



View Profile WWW
« Reply #167 on: January 17, 2018, 03:37:07 PM »

I got an opportunity to talk to Michael Cox, marketing guy from Crows Crows Crows. He's @DevMicco on Twitter, he has DMs open. Great guy with super helpful advice.

He told me to release the trailer, release the demo, push PR, everything, but don't launch Kickstarter. Instead funnel people into Discord and mailing lists. then launch Kickstarter when there's enough people to make it succeed on the first day.
Technically, is Discord necessary if you have the following on various social media? 

Either way, how would you quantify "enough"?  Certin Number of subscribers?
Logged

etodd
Level 3
***



View Profile WWW
« Reply #168 on: January 18, 2018, 06:49:18 AM »

Technically, is Discord necessary if you have the following on various social media?

Either way, how would you quantify "enough"?  Certin Number of subscribers?

Here's Michael's GDC talk explaining it.





It's something like this: you start with the number of backers needed. At $20 a piece, I need at least 1100 backers. Then you solve this equation for n: n * a * b = 1100

Where n is the number of followers I need, a is the percentage of followers who will open and read stuff I send them, and b is the percentage of those people who will actually back the project.

You can use general industry standard numbers for a and b to get an estimate of how big of a following you need. The number is quite large.
Logged

io3 creations
Level 10
*****



View Profile WWW
« Reply #169 on: January 18, 2018, 04:06:07 PM »

Thank you for your answer, that makes sense.

Also, you probably don't need to have the full KS amount covered based on your followers.  Assuming can get a good initial pledging/sharing momentum from them, that would make the campaign easier as well.  That could be useful if it took a really long time to reach the "enough" number of follower, but you wanted to get started for some reason.  Also, if the KS campaign fails but manages to collect a significant amount, relaunches tend to be successful.  Just listing a few things to keep in mind as possibilities.
Logged

etodd
Level 3
***



View Profile WWW
« Reply #170 on: January 20, 2018, 04:11:55 PM »

...

Absolutely. But it would be good for my nerves to know the Kickstarter has a good chance of success before launching it, instead of hoping for lady luck to smile on it. Smiley

New announce trailer is coming Monday. Keep an eye out. Smiley
Logged

etodd
Level 3
***



View Profile WWW
« Reply #171 on: January 22, 2018, 04:43:39 AM »

Here it is folks!! Official announce trailer. Smiley



Logged

etodd
Level 3
***



View Profile WWW
« Reply #172 on: January 29, 2018, 08:29:28 AM »

Got some coverage on PC Gamer and Rock Paper Shotgun this morning. Smiley

http://www.pcgamer.com/deceiver-is-a-philosophical-shooter-that-lets-you-shoot-drones-through-enemies/

https://www.rockpapershotgun.com/2018/01/29/deceiver-is-a-neat-looking-parkour-game-with-added-spiderbots/
Logged

etodd
Level 3
***



View Profile WWW
« Reply #173 on: January 29, 2018, 12:35:44 PM »

And Kotaku! https://www.kotaku.com.au/2018/01/854413/
Logged

Shotgun Anaconda
Level 0
**


Shotgun Anaconda


View Profile WWW
« Reply #174 on: January 29, 2018, 12:42:25 PM »

I must say the 3d art combined with the palette is really cool! Reminds me of Rez on dreamcast Smiley

Good job!
Logged
etodd
Level 3
***



View Profile WWW
« Reply #175 on: January 30, 2018, 09:54:04 AM »

I must say the 3d art combined with the palette is really cool! Reminds me of Rez on dreamcast Smiley

Good job!

Thanks! Glad you like the art style. It got a lot of hate from /r/games, so that's encouraging to hear. Smiley

Here's what's new!

Sever queues

I wanted to provide players with more feedback during the connection process, so if it fails at any point and they complain to me, I can help diagnose the problem. I also wanted to allow players to queue up and wait in line for a free slot in a server if it's full. Both of these turned out surprisingly easy to write. The whole thing got done on stream:





Breakable glass

I always loved shattering windows in F.E.A.R. They shattered realistically at the exact point you hit them. I wanted the same thing for DECEIVER. I started with a very basic system that created four dynamic meshes depending on where you shot the window:



This part was also done on stream.





Then came more subdivisions, physics bodies and impulses, network syncing, and sound effects:



Drone collision penetration

The last update to the drone collision code made it much easier to hit enemy drones. As long as your shield touches their shield, you'll do damage. Unfortunately this had some unforeseen consequences.

Since your shield is spherical and centered around your drone, it actually extends inside the surface your drone is attached to. If you're attached to a thin wall, the shield can actually stick out on the other side of the wall, exposing you to danger.

So far I've addressed this by making sure every wall is at least 0.5 meters thick, but with the new update, drones can hit each other up to 1.2 meters apart.

Here's an example:



The drone on the left is clearly aiming at an environment surface, not at the other player. But since it only needs to get within 1.2 meters of the enemy, it can actually do damage through the surface there.

Long story short, I added an extra raycast check to prevent this.

Attaching stuff to minions

So, attaching grenades to minions was fun. Side note: I changed their AI to make them melee-only when they're carrying a grenade, so they'll walk right into the fray instead of attacking at range.

I figured, why not attach other stuff to them too? So now you can attach rectifiers and even force fields to friendly OR enemy minions.

When I first tried attaching something to an enemy minion, it could not deal.



The AI has since been updated so that stuck minions accept their position in life.
Logged

etodd
Level 3
***



View Profile WWW
« Reply #176 on: February 07, 2018, 02:20:00 PM »

Despina

I'll continue to improve and add things to this map, but it's done for now. It's the largest yet.







I tried to make the architecture make sense, but in the end it turned out extremely "video game level". This will be one of the three maps available in the demo.

Reverb improvements

One problem with the reverb system was that it could only support one ambient background sound. It couldn't differentiate between indoor and outdoor ambience. So in addition to the three reverb parameters stored in each voxel cell, I added a fourth parameter that indicates the amount of "outdoorness".

In the process, I ended up throwing out almost all the reverb voxel generation code and starting over. The result is cleaner and more accurate, although there are still some cases where it fails.

Playtesting

Last Friday I streamed with Will Handford and Jessica Osborne, the two lovely voice actors who worked on the trailer. We played the game for a few hours, along with a handful of other beta playtesters.

There were zero technical problems to my knowledge! There were also no major gameplay complaints, although I still came away with a list of changes to make. The main issue at this point is that the game has a speed problem. It's 100% intensity all the time. There's never any reason to hang back, you just push and push.

I'm trying to address this problem by increasing the player crawl speed and slowing all the cooldowns. This encourages/forces people to just crawl around rather than constantly launch everywhere all the time.

Another change I made to address the issue is the waiting room.

Waiting room

Before each match, the game dumps all the players into a relaxing mode where all you can do is parkour around the map and get familiar with it before playing it for real. By default, there's a 5 minute time limit, although that can be changed. Once everyone indicates they're ready, the game starts.

The parkour code had not been designed with multiple players or networking in mind, so it required some adjustments. I ended up coding most of it on stream:



Logged

Shotgun Anaconda
Level 0
**


Shotgun Anaconda


View Profile WWW
« Reply #177 on: February 08, 2018, 01:50:24 AM »

I must say the 3d art combined with the palette is really cool! Reminds me of Rez on dreamcast Smiley

Good job!

Thanks! Glad you like the art style. It got a lot of hate from /r/games, so that's encouraging to hear. Smiley


To be honest, people dont really know what they want until they have it. Keep doing your thing Smiley
Logged
etodd
Level 3
***



View Profile WWW
« Reply #178 on: February 13, 2018, 08:01:07 AM »

To be honest, people dont really know what they want until they have it. Keep doing your thing Smiley

Thanks for the encouragement, it's much appreciated Smiley
Logged

etodd
Level 3
***



View Profile WWW
« Reply #179 on: February 20, 2018, 06:07:45 AM »

The Poor Man's Netcode

The more you know about a given topic, the more you realize that no one knows anything.

For some reason (why God, why?) my topic of choice is game development. Everyone in that field agrees: don't add networked multiplayer to an existing game, you drunken clown.

Well, I did it anyway because I hate myself. Somehow it turned out great. None of us know anything.

Problem #1: assets

My first question was: how do I tell a client to use such-and-such mesh to render an object? Serialize the whole mesh? Nah, they already have it on disk. Send its filename? Nah, that's inefficient and insecure. Okay, just a string identifier then?

Fortunately, before I had time to implement any of my own terrible ideas, I watched a talk from Mike Acton where he mentions the danger of "lazy decision-making". One of his points was: strings let you lazily ignore decisions until runtime, when it's too late to fix.

If I rename a texture, I don't want to get a bug report from a player with a screenshot like this:



I had never thought about how powerful and complex strings are. Half the field of computer science deals with strings and what they can do. They usually require a heap allocation, or something even more complex like ropes and interning. I usually don't bother to limit their length, so a single string expands the possibility space to infinity, destroying whatever limited ability I had to predict runtime behavior.

And here I am using these complex beasts to identify objects. Heck, I've even used strings to access object properties. What madness!

Long story short, I cultivated a firm conviction to avoid strings where possible. I wrote a pre-processor that outputs header files like this at build time:

Code:
namespace Asset
{
namespace Mesh
{
const int count = 3;
const AssetID player = 0;
const AssetID enemy = 1;
const AssetID projectile = 2;
}
}

So I can reference meshes like this:

Code:
renderer->mesh = Asset::Mesh::player;

If I rename a mesh, the compiler makes it my problem instead of some poor player's problem. That's good!

The bad news is, I still have to interact with the file system, which requires the use of strings. The good news is the pre-processor can save the day.

Code:
const char* Asset::Mesh::filenames[] =
{
"assets/player.msh",
"assets/enemy.msh",
"assets/projectile.msh",
0,
};

With all this in place, I can easily send assets across the network. They're just numbers! I can even verify them.

Code:
if (mesh < 0 || mesh >= Asset::Mesh::count)
net_error(); // just what are you trying to pull, buddy?

Problem #2: object references

My next question was: how do I tell a client to please move/delete/frobnicate "that one object from before, you know the one". Once again, I was lucky enough to hear from smart people before I could shoot myself in the foot.

From the start, I knew I needed a bunch of lists of different kinds of objects, like this:

Code:
Array<Turret> Turret::list;
Array<Projectile> Projectile::list;
Array<Avatar> Avatar::list;

Let's say I want to reference the first object in the Avatar list, even without networking, just on our local machine. My first idea is to just use a pointer:

Code:
Avatar* avatar;

avatar = &Avatar::list[0];

This introduces a ton of non-obvious problems. First, I'm compiling for a 64 bit architecture, which means that pointer takes up 8 whole bytes of memory, even though most of it is probably zeroes. And memory is the number one performance bottleneck in games.

Second, if I add enough objects to the array, it will get reallocated to a different place in memory, and the pointer will point to garbage.

Okay, fine. I'll use an ID instead.

Code:
template<typename Type> struct Ref
{
short id;
inline Type* ref()
{
return &Type::list[id];
}

// overloaded "=" operator omitted
};

Ref<Avatar> avatar = &Avatar::list[0];

avatar.ref()->frobnicate();

Second problem: if I remove that Avatar from the list, some other Avatar will get moved into its place without me knowing. The program will continue, blissfully and silently screwing things up, until some player sends a bug report that the game is "acting weird". I much prefer the program to explode instantly so I at least get a crash dump with a line number.

Okay, fine. Instead of actually removing the avatar, I'll put a revision number on it:

Code:
struct Avatar
{
short revision;
};

template<typename Type> struct Ref
{
short id;
short revision;
inline Type* ref()
{
Type* t = &Type::list[id];
return t->revision == revision ? t : nullptr;
}
};

Instead of actually deleting the avatar, I'll mark it dead and increment the revision number. Now anything trying to access it will give a null pointer exception. Perfect!

Now, serializing a reference across the network is just a matter of sending two easily verifiable numbers.

Problem #3: delta compression

If I had to cut this article down to one line, it would just be a link to Glenn Fiedler's blog.

Which by the way is here: gafferongames.com

As I set out to implement my own version of Glenn's netcode, I read this article, which details one of the biggest challenges of multiplayer games. Namely, if you just blast the entire world state across the network 60 times a second, you could gobble up 17 mbps of bandwidth. Per client.

Delta compression is one of the best ways to cut down bandwidth usage. If a client already knows where an object is, and it hasn't moved, then I don't need to send its position again.

This can be tricky to get right.



The first part is the trickiest: does the client really know where the object is? Just because I sent the position doesn't mean the client actually received it. The client might send an acknowledgement back that says "hey I received packet #218, but that was 0.5 seconds ago and I haven't gotten anything since."

So to send a new packet to that client, I have to remember what the world looked like when I sent out packet #218, and delta compress the new packet against that. Another client might have received everything up to packet #224, so I can delta compress the new packet differently for them. Point is, we need to store a whole bunch of separate copies of the entire world.

Someone on Reddit asked "isn't that a huge memory hog"?

No, it is not.

Actually I store 255 world copies in memory. All in a single giant array. Not only that, but each copy has enough room for the maximum number of objects (2048) even if only 2 objects are active.

If you store an object's state as a position and orientation, that's 7 floats. 3 for XYZ coordinates and 4 for a quaternion. Each float takes 4 bytes. My game supports up to 2048 objects. 7 floats * 4 bytes * 2048 objects * 255 copies = ...

14 MB. That's like, half of one texture these days.

I can see myself writing this system five years ago in C#. I would start off immediately worried about memory usage, just like that Redditor, without stopping to think about the actual data involved. I would write some unnecessary, crazy fancy, bug-ridden compression system.

Taking a second to stop and think about actual data like this is called Data-Oriented Design. When I talk to people about DOD, many immediately say, "Woah, that's really low-level. I guess you want to wring out every last bit of performance. I don't have time for that. Anyway, my code runs fine." Let's break down the assumptions in this statement.

Assumption 1: "That's really low-level".

Look, I multiplied four numbers together. It's not rocket science.

Assumption 2: "You sacrifice readability and simplicity for performance."

Let's picture two different solutions to this netcode problem. For clarity, let's pretend we only need 3 world copies, each containing up to 2 objects.

Here's the solution I just described. Everything is statically allocated in the .bss segment. It never moves around. Everything is the same size. No pointers at all.



Here's the idiomatic C# solution. Everything is scattered randomly throughout the heap. Things can get reallocated or moved right in the middle of a frame. The array is jagged. 64-bit pointers all over the place.



Which is simpler?

The second diagram is actually far from exhaustive. C#-land is a lot more complex in reality. Check the comments and you'll probably find someone correcting me about how C# actually works.

But that's my point. With my solution, I can easily construct a "good enough" mental model to understand what's actually happening on the machine. I've barely scratched the surface with the C# solution. I have now idea how it will behave at runtime.

Assumption 3: "Performance is the only reason you would code like this."

To me, performance is a nice side benefit of data-oriented design. The main benefit is clarity of thought. Five years ago, when I sat down to solve a problem, my first thought was not about the problem itself, but how to shoehorn it into classes and interfaces.

I witnessed this analysis paralysis first-hand at a game jam recently. My friend got stuck designing a grid for a 2048-like game. He couldn't figure out if each number was an object, or if each grid cell was an object, or both. I said, "the grid is an array of numbers. Each operation is a function that mutates the grid." Suddenly everything became crystal clear to him.

Assumption 4: "My code runs fine".

Again, performance is not the main concern, but it's important. The whole world switched from Firefox to Chrome because of it.

Try this experiment: open up calc.exe. Now copy a 100 MB file from one folder to another.



I don't know what calc.exe is doing during that 300ms eternity, but you can draw your own conclusions from my two minutes of research: calc.exe actually launches a process called Calculator.exe, and one of the command line arguments is called "-ServerName".

Does calc.exe "run fine"? Did throwing a server in simplify things at all, or is it just slower and more complex?

I don't want to get side-tracked. The point is, I want to think about the actual problem and the data involved, not about classes and interfaces. Most of the arguments against this mindset amount to "it's different than what I know".

Problem #4: lag

I now hand-wave us through to the part of the story where the netcode is somewhat operational.

Right off the bat I ran into problems dealing with network lag. Games need to respond to players immediately, even if it takes 150ms to get a packet from the server. Projectiles were particularly useless under laggy network conditions. They were impossible to aim.

I decided to re-use those 14 MB of world copies. When the server receives a command to fire a projectile, it steps the world back 150ms to the way the world appeared to the player when they hit the fire button. Then it simulates the projectile and steps the world forward until it's up to date with the present. That's where it creates the projectile.

I ended up having the client create a fake projectile immediately, then as soon as it hears back from the server that the projectile was created, it deletes the fake and replaces it with the real thing. If all goes well, they should be in the same place due to the server's timey-wimey magic.

Here it is in action. The fake projectile appears immediately but goes right through the wall. The server receives the message and fast-forwards the projectile straight to the part where it hits the wall. 150ms later the client gets the packet and sees the impact particle effect.



The problem with netcode is, each mechanic requires a different approach to lag compensation. For example, my game has an "active armor" ability. If players react quick enough, they can reflect damage back at enemies.



This breaks down in high lag scenarios. By the time the player sees the projectile hitting their character, the server has already registered the hit 100ms ago. The packet just hasn't made it to the client yet. This means you have to anticipate incoming damage and react long before it hits. Notice in the gif above how early I had to hit the button.

To correct this, the server implements something I call "damage buffering". Instead of applying damage instantly, the server puts the damage into a buffer for 100ms, or whatever the round-trip time is to the client. At the end of that time, it either applies the damage, or if the player reacted, reflects it back.

Here it is in action. You can see the 200ms delay between the projectile hitting me and the damage actually being applied.



Here's another example. In my game, players can launch themselves at enemies. Enemies die instantly to perfect shots, but they deflect glancing blows and send you flying like this:



Which direction should the player bounce? The client has to simulate the bounce before the server knows about it. The server and client need to agree which direction to bounce or they'll get out of sync, and they have no time to communicate beforehand.

At first I tried quantizing the collision vector so that there were only six possible directions. This made it more likely that the client and server would choose the same direction, but it didn't guarantee anything.

Finally I implemented another buffer system. Both client and server, when they detect a hit, enter a "buffer" state where the player sits and waits for the remote host to confirm the hit. To minimize jankiness, the server always defers to the client as to which direction to bounce. If the client never acknowledges the hit, the server acts like nothing happened and continues the player on their original course, fast-forwarding them to make up for the time they sat still waiting for confirmation.

This system is technically hackable. You could alter you client to never acknowledge hits. But I can't think of another solution that doesn't compromise the experience of legitimate players. Also, I'm not sure the hack would be particularly useful compared to more traditional hacks.

Problem #5: jitter

My server sends out packets 60 times per second. What about players whose computers run faster than that? They'll see jittery animation.

Interpolation is the industry-standard solution. Instead of immediately applying position data received from the server, you buffer it a little bit, then you blend smoothly between whatever data that you have.

In my previous attempt at networked multiplayer, I tried to have each object keep track of its position data and smooth itself out. I ended up getting confused and it never worked well.

This time, since I could already easily store the entire world state in a struct, I was able to write just two functions to make it work. One function takes two world states and blends them together. Another function takes a world state and applies it to the game.

How big should the buffer delay be? I originally used a constant until I watched

where they mention adaptive interpolation delay. The buffer delay should smooth out not only the framerate from the server, but also any variance in packet delivery time.

This was an easy win. Clients start out with a short interpolation delay, and any time they're missing a packet to interpolate toward, they increase their "lag score". Once it crosses a certain threshold, they tell the server to switch them to a higher interpolation delay.

Of course, automated systems like this often act against the user's wishes, so it's important to add switches and knobs to the algorithm!



Problem #6: joining servers mid-match

Wait, I already have a way to serialize the entire game state. What's the hold up?

Turns out, it takes more than one packet to serialize a fresh game state from scratch. And each packet may take multiple attempts to make it to the client. It may take a few hundred milliseconds to get the full state, and as we've seen already, that's an eternity. If the game is already in progress, that's enough time to send 20 packets' worth of new messages, which the client is not ready to process because it hasn't loaded yet.

The solution is—you guessed it—another buffer.

I changed the messaging system to support two separate streams of messages in the same packet. The first stream contains the map data, which is processed as soon as it comes in.

The second stream is just the usual fire-hose of game messages that come in while the client is loading. The client buffers these messages until it's done loading, then processes them all until it's caught up.

Problem #7: cross-cutting concerns

This next part may be the most controversial.

Remember that bit of gamedev wisdom from the beginning? "don't add networked multiplayer to an existing game"?

Well, most of the netcode in this game is literally tacked on. It lives in its own 5000-line source file. It reaches into the game, pokes stuff into memory, and the game renders it.

Just listen a second before stoning me. Is it better to group all network code in one place, or spread it out inside each game object?

I think both approaches have advantages and disadvantages. In fact, I use both approaches in different parts of the game, for various reasons human and technical.

But some design paradigms (*cough* OOP) leave no room for you to make this decision. Of course you put the netcode inside the object! Its data is private, so you'll have to write an interface to access it anyway. Might as well put all the smarts in there too.

Conclusion

I'm not saying you should write netcode like I do; only that this approach has worked for me so far. Read the code and judge for yourself.

There is an objectively optimal approach for each use case, although people may disagree on which one it is. You should be free to choose based on actual constraints rather than arbitrary ones set forth by some paradigm.

Thanks for reading. DECEIVER is launching on Kickstarter soon. Sign up to play the demo here!
Logged

Pages: 1 ... 7 8 [9] 10
Print
Jump to:  

Theme orange-lt created by panic