đź““

Setting up the Player Prefab

A Player in this engine must have a component/script attached extending the PlayerNetworkEntity class, such as the example CyborgPlayer which we can see lets clients control a 4 directional melee character. You can look at that class for reference, but lets build a simple one together and get it working.

build new gameobject

Open Scenes→TestingGround or make a new Scene in Unity. We’re not worried about the Scene so much, its just a spot to drag things into.

Next go to 2d assets and find a sprite to drag into the editor. For example in Cyborg folder, expand Idles asset and drag idles_1 into the Scene somewhere

You can use your own assets, these ones have just be added to the sample project already and have been sliced already. Anyway, this creates a GameObject with a SpriteRenderer component. Lets rename this GameObject to something else like TestPlayer

There are some ordering settings setup in Unity for this type of game, but to make sure this character is ordered above the background we should set Order In Layer on the Sprite Renderer to something higher then 0. How about 5

Currently the game sorts things on the y axis, things further down the screen are put closer to the camera to give the fake 3d illusion lots of 2d games use. If you want to change that though or want to know how I set it up, go to Project Settings→Graphics→Transparency Sort Mode and tweak those. Here’s what I have there for the sample project

Add Components we need

Ok, its a good start but no where near done. Lets add a couple of basic components. Go to Assets→Scripts→Cyborg Netcode→Components and drag PlayerPhysicsComponent and AnimationComponent onto your TestPlayer gameObject

(note I dragged Player prefab in for reference when making this guide, so ignore them)

Setting up Physics Component

We can set a few values in here soon, but before we do, we’ll need a collision box of sorts so our player knows how to bump into things in the world. Go to Assets→Scripts→Cyborg Netcode→Math and Shaps and drag ObjectRect onto TestPlayer.

Set some values so that it fits over the character nicely. I use these:

Then drag that ObjectRect into the CollisionBox section of the PlayerPhysicsComponent

I usually set a few other things, like saying we can bump into things and change the friction multiplier. We’ll set the PlayerNetworkEntity later.

Sweet, onto the other components setup

Setting up Animation Component

Set Animation Set Name to something, I went with Base, and set the Sprite Renderer to the one attached to TestPlayer(either click the dot-circle thing and find TestPlayer or drag TestPlayer’s SpriteRenderer into this slot):

We’ll setup more for here later as we’ll have to choose whether to use Unity’s animation system or the one I built for this project.

Add New Class and fill it in

Ok, now its time for some fun. Lets build the thing where our logic is going to go, we’re going to extend the PlayerNetworkEntity class! To do that click the Add Component button at the bottom of our TestPlayer’s inspector info

Choose New Script and this window should pop up, fill in TestPlayer as the script name and hit Create and Add button

It will have added the script as a component to your TestPlayer game object. Not super exciting yet, so open up the TestPlayer script in Visual Studio or whatever you use on your end. Once its open, it should look similar to this

Boring! Lets set this thing up. Lets add some new library references at the top and then make this class extend(inherit from) PlayerNetworkEntity. You’ll probably get a red wiggly line, ignore that for now

If you are using Visual Studio and hover over TestPlayer with the red line, it will suggest implementing a few abstract methods inherited from PlayerNetworkEntity

If you can, do that option, if not, type this out. Oh and remove the exception stuff from within them.

Cool, we have 2 methods we have to fill, plus a bunch we probably should override from PlayerNetworkEntity. We’ll get to that later. We’re just going to try get a little guy to walk around. So lets set a moveSpeed attribute which will help us control movement. (I’ll start using code sections so you can copy and paste if you need to)

public class TestPlayer : PlayerNetworkEntity
{
public sfloat moveSpeed = (sfloat)0.24f;
//...

We will also need to override the DoInput method from our parent class, currently it doesn’t do much, so we’ll make it useful. We’d like to move our character around, lets move towards that.

public override void DoInput(PlayerInput input)
    {
        base.DoInput(input);
        if (mostRecentInput != null)
        {
            Walk(input);

            if (mostRecentInput.A == InputState.pressed)
            {
                //what happens when A is pressed
            }
            if (mostRecentInput.B == InputState.pressed)
            {
                //what happens when B is pressed
            }
            if (mostRecentInput.C == InputState.pressed)
            {
                //what happens when C is pressed
            }
            //ADD MORE IF YOU NEED MORE

            
            mostRecentInput = null;
        }
    }

We haven’t implemented the method Walk yet! So it should be red or something. But at least we have this here ready to fill in, especially if we want to add button press events down the track. Lets make this Walk method

private void Walk(PlayerInput input)
    {
        //horizontal movement
        if (input.moveAxis.x >= 0.5f)
        {
            physicsComponent.instantVelocity.x = moveSpeed;
        }
        else if (input.moveAxis.x <= -0.5f)
        {
            physicsComponent.instantVelocity.x = -moveSpeed;
        }
        else
        {
            physicsComponent.instantVelocity.x = (sfloat)0;
        }
        //vertical
        if (input.moveAxis.y >= 0.5f)
        {
            physicsComponent.instantVelocity.y = moveSpeed;
        }
        else if (input.moveAxis.y <= -0.5f)
        {
            physicsComponent.instantVelocity.y = -moveSpeed;
        }
        else
        {
            physicsComponent.instantVelocity.y = (sfloat)0;
        }
        
    }

Pretty simple but does the trick. We’re treating it as if a gamepad stick may be used with a deadzone of 0.5, whereas keyboard will always give -1,0,1 values so it works for both. We’re setting instantVelocity because I tend to find that in a game we have external forces acting our a player(wind, water, gravity etc), but as a player we want to move ourselves and fight against those forces. This can be done by setting the acceleration, but for snappier movement I prefer to just set the velocity. The finalVelocity to work out the players new position at the end of a frame is finalVelocity = velocity + instantVelocity. So they fight against each other, but wont slow or increase the other, just the final result.

Look through the physicsComponent if you want to see what you can play with.

Well the PhysicsComponent attached to our TestPlayer is not going to be very useful at this stage if we don’t update it. Its time to fill in UpdateEntity method

public override void UpdateEntity()
    {
        //if not our player and client side is doing a roll back, don't update stuff here
        if (!IsMyPlayer() && NetworkEntityManager.IsRollbacking())
        {
            return;
        }

        physicsComponent.UpdateComponent();
    }

UpdateEntity is a method that gets called per gameTick, so this is the place to update timers or do other per frame logic. In another guide I’ll show some basic AI logic put into this section as an example. But the player for the moment really does mostly just respond to inputs for now, so its a bit empty for the time being.

Lets do some house keeping before we get onto anything crazy. Well our physicsComponent won’t do us much good, if it doesn’t know which STransform(position, rotation, scale) to update. We set that up on Unity GameObject Start to ensure it gets called as soon as the component is setup.

We’re also going to fill in Update. By default each NetworkEntity is using Snapshot Lerp for its interpolation. Well if we’re client we don’t want to get our character interpolated, we control them! And the server doesn’t want any players its simulating using any lerping because it runs things in real time. Lets quickly fill that in. OH its also handy to let the PlayerInputManager know which TestPlayer in your game(multiplayer remember, could be heaps) is the current one you are controlling.

// Start is called before the first frame update
    void Start()
    {
        //setup components
        physicsComponent.Init(sTransform);
    }

    // Update is called once per frame
    public void Update()
    {
        //I control my player
        if (IsMyPlayer())
        {

            //ASSIGN ME AS THE PLAYABLE PLAYER
            PlayerInputManager.SetPlayerEntity(this);


            //dont lerp me bro
            physicsComponent.lerpType = LerpType.none;

        }
        else if (NetworkManager.GetConnectionType() == ConnectionType.server)
        {

            //server doesn't lerp, runs all entities in realtime
            physicsComponent.lerpType = LerpType.none;
        }
        else if (NetworkManager.GetConnectionType() == ConnectionType.client)
        {
            //or lerp if not mine and client side

        }
    }

Now the tricky stuff, on the server when a SnapShot(info about all NetworkEntities in the game at a point in time) is saved and shoved into a packet, well, we need to decide what data about each individual entity type we should save and send. This is where you can see some funky stuff go wrong, so remember this rule. Whatever you write to a packet, you need the read function to get it in the same order!

Ok, so what data do I need to write? We need to send at least the send the EntityID and typeID for this thing to work. That helps it find the right entity on the other end to read the correct data. Since this is a type of PlayerNetworkEntity might be handy to send PlayerID too and since they move around the map, lets also write the position. There is this concept of writing a full packet or not depending on if you are the client who owns that particular PlayerNetworkEntity or not, but refer to CyborgPlayer class for help on that. Lets just get the minimum going. so fill in public override void WriteToPacket(NetDataWriter writer, bool fullWrite) but also override the basic one too as follows.

public override void WriteToPacket(NetDataWriter writer)
    {
        WriteToPacket(writer, false);
    }
    public override void WriteToPacket(NetDataWriter writer, bool fullWrite)
    {
        writer.Put(entityID);
        writer.Put(type.typeID);
        writer.Put(playerID);
        //whether its the full packet or not
        writer.Put(fullWrite);
        //pos
        writer.Put(sTransform.position.x.RawValue);
        writer.Put(sTransform.position.y.RawValue);
        writer.Put(sTransform.position.z.RawValue);
        
        //instal vel
        writer.Put(physicsComponent.instantVelocity.x.RawValue);
        writer.Put(physicsComponent.instantVelocity.y.RawValue);
        writer.Put(physicsComponent.instantVelocity.z.RawValue);
        //split the write logic here for extra stuff
        if (fullWrite)
        {

            //vel
            writer.Put(physicsComponent.velocity.x.RawValue);
            writer.Put(physicsComponent.velocity.y.RawValue);
            writer.Put(physicsComponent.velocity.z.RawValue);
            //acc
            writer.Put(physicsComponent.acceleration.x.RawValue);
            writer.Put(physicsComponent.acceleration.y.RawValue);
            writer.Put(physicsComponent.acceleration.z.RawValue);

            //localised forces
            //local vel
            writer.Put(physicsComponent.localVelocity.x.RawValue);
            writer.Put(physicsComponent.localVelocity.y.RawValue);
            writer.Put(physicsComponent.localVelocity.z.RawValue);
            //local acc
            writer.Put(physicsComponent.localAcceleration.x.RawValue);
            writer.Put(physicsComponent.localAcceleration.y.RawValue);
            writer.Put(physicsComponent.localAcceleration.z.RawValue);
        }
    }

Info about a NetworkEntity is sent from Server to Client via a UDP packet and read on the clients end. The read should mirror the write except for one difference; the EntityID and TypeID have already been read(consumed) to work out which NetworkEntity class to send it to, to read the rest. so we skip those 2, but otherwise must read the data in the write

public override void ReadFromPacket(NetDataReader reader)
    {

        playerID = reader.GetInt();
        SVector previousPosition = sTransform.position.Copy();

        bool doFullRead = false;
        //if full read packet or abridged
        doFullRead = reader.GetBool();

        //pos
        sTransform.position.x = sfloat.FromRaw(reader.GetUInt());
        sTransform.position.y = sfloat.FromRaw(reader.GetUInt());
        sTransform.position.z = sfloat.FromRaw(reader.GetUInt());
        //instant vel
        physicsComponent.instantVelocity.x = sfloat.FromRaw(reader.GetUInt());
        physicsComponent.instantVelocity.y = sfloat.FromRaw(reader.GetUInt());
        physicsComponent.instantVelocity.z = sfloat.FromRaw(reader.GetUInt());
        if (doFullRead)
        {
            //vel
            physicsComponent.velocity.x = sfloat.FromRaw(reader.GetUInt());
            physicsComponent.velocity.y = sfloat.FromRaw(reader.GetUInt());
            physicsComponent.velocity.z = sfloat.FromRaw(reader.GetUInt());
            //acc
            physicsComponent.acceleration.x = sfloat.FromRaw(reader.GetUInt());
            physicsComponent.acceleration.y = sfloat.FromRaw(reader.GetUInt());
            physicsComponent.acceleration.z = sfloat.FromRaw(reader.GetUInt());

            
            //vel
            physicsComponent.localVelocity.x = sfloat.FromRaw(reader.GetUInt());
            physicsComponent.localVelocity.y = sfloat.FromRaw(reader.GetUInt());
            physicsComponent.localVelocity.z = sfloat.FromRaw(reader.GetUInt());
            //acc
            physicsComponent.localAcceleration.x = sfloat.FromRaw(reader.GetUInt());
            physicsComponent.localAcceleration.y = sfloat.FromRaw(reader.GetUInt());
            physicsComponent.localAcceleration.z = sfloat.FromRaw(reader.GetUInt());
            

        }
        //make sure we update our physicsComponent appropriately to help with any lerping
        physicsComponent.StoreMostRecentDataFromServer();
        physicsComponent.SetupLerpFromPacketData(previousPosition);
    }

SAVE! Its time to go back to Unity.

If you did everything right, your TestPlayer should look like this AND there should be no errors in the console tab/view.

We need to now fill in the Type, PhysicsComponent and Type on here. Either drag their references in from the slots around or click the circle dot thing and select them. The goal here is to replace the Player prefab with your own, so we’ll re-use that Player NetworkEntityType for now. Anyway fill in the blanks like in the below image.

Create Prefab

Ok, we’ve done most of the work. Drag TestPlayer from the scene into the Prefabs→NetworkEntities folder to create a prefab for it

Now select NetworkEntityPrefabs. This file helps our client and server know how to build prefabs on the go in game. We’re going to replace Player inside of this thing, so once you have NetworkEntityPrefabs inspector up, drag TestPlayer into the Element 0 slot

I need to de-couple this class combo, but we need to let the PlayerPhysicsComponent know about the TestPlayer(PlayerNetworkEntity) class its linked to, so set this reference too

Save! Guess we can test now

TEST

The full guides to testing are here but lets just do a basic test. Open Scenes→Join and hit play. The quickest way to see that it kinda works is running a server, so choose Host Game

And then check can server be a player too and click Host Private Game because we don’t want to list this as a joinable game anywhere.

The server sets up and should launch you into the first map where you should have control over a TestPlayer who can move around and not much else

If you have errors, go back through the previous steps. This doesn’t actually check any networked play though. For that we need a Client and a Server running. Follow this guide to test that: Testing Client and Server Locally. If the client also works, then well done! If not, its probably the Read/Write packet stuff from above.

That’s enough for now!

From here, its probably a good time to jump into another one of the guides or experiment for a bit. Maybe try to get this character animated from one of the animation guides