Game Development Community

Having trouble with simple script concept: client versus server

by Justin Tolchin · in RTS Starter Kit · 03/16/2005 (3:06 pm) · 6 replies

Hi all,

Ok, I'm feeling pretty dumb right about now, so bear with me! I'm confused about how the script code is executed in terms of when its executed for a client object as opposed to its corresponding server object. I have some code in player.cs (which is a server-side script, right? since it's in \server\scripts\avatars) but it's getting called at one point for a client-side object (an RTSUnit).

The specific code being invoked is the "Player::onReachDestination" script. Shouldn't that only get called for a server-side object? On the other hand, if that were true, why are there "setActionThread" calls in the player.cs code? Wouldn't only the client care about the animation thread? Or is "setActionThread" really server-side but it somehow does something to update the client as well? Or is the player.cs code really intended to be shared between client and server?

I'm trying to keep track of each RTSUnit's "state" on the server side, but somehow my "popState" calls are getting invoked (via a call from onReachDestination) for the client-side object, which of course fails since the "state"-related fields don't exist on the client object.

As you can see, I'm really puzzled about what gets executed where. Can someone give me the basic run-down of what is supposed to happen when you give an RTSUnit a simple move order? I assumed it would perform the move on the server and send commands down to the client to update the position, activate and deactive animation, etc., but maybe that's not at all how it works.

Can someone clear this up for me? Thanks!!!

#1
03/16/2005 (3:36 pm)
That is actually how it works...all of the player movement is done server side, and the movement of units is transmitted via object ghosting to all clients in scope.

How do you know that a particular function call is happening on the client side, vs the server side? It can be incredibly confusing when you are running the server and the client co-located (in the same executable). One technique to prove this is to use the print out the %object value for each object when your calls are occuring, along with a CLIENT: or SERVER: tag in your echo statements. If you do this rigorously, you'll note that the object id's are different on the server side from the client side, and if you had another client connected to your game, all client id's would be different for that client as well, even if it is the same "object" that is being called--in other words, for every server object, each client has a ghost (if in scope), and the ghost object's id's are different for every client.

setActionThreads are called server side again because these are things that can affect one or more clients, since you are setting the action thread on a server object, and then the change is propagated to each of the clients over your ghosted object networking.

The summary of events when you want to give an RTSUnit a move order:

1) Client selects the unit. Sends "add to selection" call to the server.
2) Client right clicks on terrain. Sends an IssueMove commandToServer (note: this is NOT for each unit that is selected, but a single command that the server will apply to whatever is that client's selection group).
3) Server catches the command, iterates over the selection group, setting the move destination appropriately. In stock RTS-SK, each unit in the selection group given a separate destination based on it's current position in relation to the center of the selected group as a whole, and the new destination is adjusted by that offset.
4) As processTick is called for each of the RTSUnits (server side), they are moved incrementally towards their moveDestination. Appropriate animation threads are assigned at the server as well. Also each tick, the current state of each unit is packed up, and ghosted across the network to each client that is in Scope, so they see the units moving and animated.
5) As each unit reaches it's destination, the server side Player::onReachDestination() script is called. IIRC, the animations are turned off at the C++ level, in processTick.
#2
03/16/2005 (6:28 pm)
Hi Stephen,

Thanks for the info. That's actually basically what I thought was supposed to be happening, but I am *definitely* seeing the object's *client* id being displayed from the onReachDestination code, which I don't understand. So for example, the server side id is 1498 and the client id is 1649, and I see this in the log:

Mapping string: IssueMove to index: 7
issuing move command to unit: 1498
RTSUnit::setState called.
state: Move duration: 0 location: -42 -46 400 target: 0
RTSUnit::processState called. state = Move
Player::setMoveGoal--object (1498) now moving to (-42 -46 400)
1649 - Arrived!
starter.RTS/server/scripts/avatars/RTSUnit.cs (127): Unable to find object: '' attempting to call function 'count'

That "Unable to find object" line is coming from the state code because the object passed to it is the client-side object (apparently). Note that it's object 1649 that is printing out at the "Arrived" comment, and the code that prints that only exists in \server\scripts\avatars\player.cs. So how can that be happening?

(Also I'm noticing that I see that "Arrived" line printed out several times which doesn't make sense to me either. But that's probably a logic bug somewhere).

Still confused. :-(
#3
03/16/2005 (6:52 pm)
Ok, for this exercise to work out properly, you'll need to use a LOT more echos, and for each and every single echo, put out the object id that is being acted upon, as well as the CLIENT or SERVER tag. For example:

You should have an echo of the object's ID when it is spawned. This will show you for certain the server object id. You should also show the object ID of the object when RTSUnit::setState is called, this will keep it tracked as well.

We also cannot see your setState/processState code, so it's possible that you are getting a unit's id from the client, and not properly converting it to the server's id (resolveGhostId I think is the command) before processing it somewhere along the line.

In general, to get a full understanding of what version of the object you are using, and what's happening to it, I would suggest going through the entire set of functions that are being called and put the echo lines in each and every one of them, giving the client/server tag, the script function name, and the object id(s) being passed. Once complete, this will let you trace the "history" of an object throughout the entire execution stack, and see where there might be glaring errors.

My general suspicion is that somewhere along the line you are sending an object reference from the client to the server without converting, or the reverse, but that's just a guess.
#4
03/17/2005 (11:20 am)
Hi Stephen,

Ok, I *think* it all makes sense now. I think there was a slight bug in the RTSUnit.cc code in the ProcessTick method. It explains why I used to see lines like this in the console.log:

1498 - Arrived!
1649 - Arrived!

when only one unit was moving. There is code down around line 479 in ProcessTick that looks like this:

if(goalDelta.len() < 0.1)
{
	stopMove();
	mVelocity.set(0,0,0);
	setActionThread(PlayerData::RootAnim,true,false,false);
	Con::executef(this, 1, "onReachDestination");

	//sync up the position
	if (isServerObject())
		setMaskBits(MoveMask);
}

Shouldn't the onReachDestination call be inside in the "isServerObject" conditional? Otherwise you end up calling that method for client-side objects, which seems wrong since that's a server-side script. In any case, I changed it to

if (isServerObject())
{
	Con::executef(this, 1, "onReachDestination");
	setMaskBits(MoveMask);
}

and now my code is much happier. :-) There was another bug (in my own code) that was preventing the onReachDestination method from being called for the server-side object, which is why I only saw the "Arrived" message for the client object.

Anyway, just wanted your opinion on this. Is this a bug in RTSUnit.cc? I guess it never ended up making a difference in the stock code because most of the onReachDestination script gets skipped for a client object, since it checks things like this:
"if(%this.status $= "Collecting"...

and that field doesn't exist on the client object (I'm assuming).
#5
03/17/2005 (11:22 am)
At first glance, yes that does look like a bug, or at least something that wasn't intended!

I'd have to look deeply as to why the script succeded being called even though it was in the server side--I think it has to do with how namespaces are handled in a co-located process (server and client in one process). It really shouldn't be called client side, and in fact is not in a dedicated server/dedicated client configuration, so your fix is a good one.

EDIT: And yes, I need to update that code for the resource, since as you've demonstrated, those calls can happen on the client side, even though they shouldn't. Something like checking to see if we're on the client, and returning if so should do it.

Nice catch!
#6
03/17/2005 (2:31 pm)
Ok, cool. Good to know I wasn't completely crazy. :-)

Thanks for all the helpful advice too!