Функции клиента/сервера

Материал из CryWiki Russia

Перейти к: навигация, поиск
О статье
Авторins
СложностьСредняя
ТребованияТекстовый редактор
Дата добавления15/11/08
Последнее изменение10/06/11


Содержание

Введение

Начая скриптинг в мультиплеере, мы понимаем, что простые вещи становятся более сложными. В сингплеере, нам не нужно заботиться о подключении клиентов посреди игры, их отключении, архитектуре «Клиент/Сервер» и тому подобное.

Но, как только вы захотите добиться от многопользовательской игры всего того, что вы считает необходимым, то не стоит сразу же куда-то убегать, ведь система Lua в Crysis достаточно гибка. Эта статья объяснит вам, как работает архитектура «Клиент/Сервер» в Crysis (Lua), и то, как вы можете создать свои собственные функции, выполняющиеся от сервера к клиенту, или наоборот.

Дверь

Для начала, нам стоит посмотреть на сущность, которая уже присутствует в игре Crysis: дверь. Файл размещён здесь: Scripts/Entities/Doors/Door.lua

Net Expose

Первым кодом, связанным с сетью, мы видим функцию Net.Expose (с таблицей в качестве аргумента).

Lua

Net.Expose {
	Class = Door,
	ClientMethods = {
		ClSlide = { RELIABLE_UNORDERED, POST_ATTACH, BOOL },
		ClRotate = { RELIABLE_UNORDERED, POST_ATTACH, BOOL, BOOL },
	},
	ServerMethods = {
		SvRequestOpen = { RELIABLE_UNORDERED, POST_ATTACH, ENTITYID, BOOL },
	},
	ServerProperties = {
	},
};

Первым в таблице стоит Class. Класс определён в верхней части файла:

Lua

Door = 
{
	Properties = 
	{
		soclasses_SmartObjectClass = "Door",
 
		fileModel = "Objects/library/furnishings/doors/toiletstall_door_local.cgf",
 
		Sounds = 
		{
			soundSoundOnMove = "sounds/doors/wooddooropen.wav",
			soundSoundOnStop = "",
			soundSoundOnStopClosed = "",
			fVolume = 200,
			fRange = 50,
		},		
		Rotation = 
		{
			fSpeed = 200.0,
			fAcceleration = 500.0,
			fStopTime = 0.125,
			fRange = 90,
			sAxis = "z",
			bRelativeToUser = 1,
			sFrontAxis = "y",
		},
		Slide = 
		{
			fSpeed = 2.0,
			fAcceleration = 3.0,
			fStopTime = 0.5,
			fRange = 0,
			sAxis = "x",
		},
		fUseDistance = 2.5,
		bLocked = 0,
		bSquashPlayers = 0,
		bActivatePortal = 0,
  },
 
	PhysParams = 
	{ 
		mass = 0,
		density = 0,
	},
 
	sounds = {},
 
	Server = {},
	Client = {},
}

Имейте это в виду при создании собственных сущеостей, совместимых с мультиплеером. Вызывайте Net.Expose ПОСЛЕ определённого вами класса.

После него, мы видим методы Client и Server.

Lua

	ClientMethods = {
		ClSlide = { RELIABLE_UNORDERED, POST_ATTACH, BOOL },
		ClRotate = { RELIABLE_UNORDERED, POST_ATTACH, BOOL, BOOL },
	},
	ServerMethods = {
		SvRequestOpen = { RELIABLE_UNORDERED, POST_ATTACH, ENTITYID, BOOL },
	},

Здесь мы определяем какие функции могут вызываться через сеть.


ClientMethods
Эти функции выполняются на клиенте, по запросу сервера.
ServerMethods
Функции в этой таблице, будут выполнены на сервере, по запросу клиента.

Теперь давайте подробно рассмотрим, какую информацию требует функция Net.Expose для выполнения этих функций.

НазваниеФункции = { НАДЕЖНОСТЬ, ПРИКРЕПЛЕНИЕ, ТИПАРГУМЕНТОВ },

  • Название функции

Первое что мы видим — название функции. The unwritten rule is to prefix client-functions with Cl and server-functions with Sv. Неписаным правилом являются префиксы функций клиента с Cl и серверных функций с Sv.

  • Reliability Type
RELIABLE_ORDERED
Ensures the packets are delivered, and in the correct order.
RELIABLE_UNORDERED
Ensures the packets are delivered, but with the possibility of receiving them in a different order.
UNRELIABLE_ORDERED
Does not ensure that the packets are delivered, but makes sure they’re in the correct order if they are.

Примечание:
It seems that UNRELIABLE_UNORDERED is not defined. However if you really need to use UNRELIABLE_UNORDERED, use the number 3 instead (The reliability defines are just numbers).

The most used one is RELIABLE_UNORDERED. This will work just fine for most functions. If you’re repeatidly sending the same function and it needs to arrive in order, use RELIABLE_ORDERED.


  • Attachment Type
NO_ATTACH
POST_ATTACH
PRE_ATTACH


  • Function argument types

The last thing to do is listing the function arguments that need to be passed on the network. Possibilities are:

BOOL
A boolean value (true/false)
STRING
A string
INT8
An integer of 8 bits
INT32
An integer of 32 bits
VEC3
A vector table (with x, y and z values)
ENTITYID
An entityId value
FLOAT
A floating point value
STRINGTABLE
A table with strings
DEPENTITYID
 ???

There are probably more around, but those are the ones that are used the most.


Just keep this in mind:


Предупреждение:
When you want to execute functions from the server to the client, or the other way around, the functions need to be listed in a Net.Expose function (for Lua).

Function implementations

Let’s have a look at the actual functions now. How do they work and how do you call them?


A server function: SvRequestOpen

Lua

function Door.Server:SvRequestOpen(userId, open)
	local mode=DOOR_TOGGGLE;
	if (open) then
		mode=DOOR_OPEN;
	else
		mode=DOOR_CLOSE;
	end
 
	local user=System.GetEntity(userId);
	self:Open(user, mode);
end


The first thing to note is the .Server just behind the ClassName. The Server part is a table in the Door class (table) generated by the Crysis Lua (Network) system.

The function has to be in the Server table because we said it would be a server function in Net.Expose.

Lua

	ServerMethods = {
		SvRequestOpen = { RELIABLE_UNORDERED, POST_ATTACH, ENTITYID, BOOL },
	},

From this info, we know the first argument will be an entityid, and the second will be a boolean. If we go deeper inside the function we can see it sets the door to open or closed, depending on the argument, and then calls the Open() function. Let’s take a look at that.

Lua

function Door:Open(user, mode)
	local lastAction = self.action;
 
	if (mode == DOOR_TOGGLE) then
		if (self.action == DOOR_OPEN) then
			self.action = DOOR_CLOSE;
		else
			self.action = DOOR_OPEN;
		end
	else
		self.action = mode;
	end
 
	if (lastAction == self.action) then
		return 0;
	end
 
	if (self.Properties.Rotation.fRange ~= 0) then					
		local open=false;
		local fwd=true;
 
		if (self.action == DOOR_OPEN) then			
			if (user and (tonumber(self.Properties.Rotation.bRelativeToUser) ~=0)) then
				local userForward=g_Vectors.temp_v2;
				local myPos=self:GetWorldPos(g_Vectors.temp_v3);
				local userPos=user:GetWorldPos(g_Vectors.temp_v4);
				SubVectors(userForward,myPos,userPos);
				NormalizeVector(userForward);
 
				local dot = dotproduct3d(self.frontAxis, userForward);
 
				if (dot<0) then
					fwd=false;
				end
			end
 
			open=true;
		end
 
		self.fwd=fwd;
		self:Rotate(open, fwd);
		self.allClients:ClRotate(open, fwd);
	end
 
...
 
	return 1;
end

A lot of code here, but we will just look at the essentials. The function first sets the correct action (door state) and returns if it was the same as last time (ie: opening a door twice). Then it checks if there’s a Range value in the Door.Properties.Rotation table. If that’s the case, we have a rotating door. Next, it calls the Rotate() function, which will actually do the rotation (We haven’t done any rotating so far).

Finally we send the new door status to all clients (the door has to open/close), so every connected client can see the door opening, with

Lua

self.allClients:ClRotate(open, fwd);

This is how we execute a function from the server -> client. We want all clients to execute the ClRotate function with the supplied arguments.

Instead of sending it to every client, there are also ways to limit the number of clients to send it to.


  • To all clients

Lua

self.allClients:ClRotate(open, fwd);


  • To all clients, except 1

Lua

self.otherClients:ClRotate(channelId, open, fwd);


  • To 1 client

Lua

self.onClient:ClRotate(channelId, open, fwd);


Примечание:
channelId is a network channel number, which you can easily get from a player with playerEnt.actor:GetChannel()

To send a function to only 1 specific team, you’ll have to loop through all players and check their team, and then send a command to them individually, using the method above.


It’s time to look at the ClRotate function now.

A client function: ClRotate

Lua

function Door.Client:ClRotate(open, fwd)
	if (not self.isServer or g_gameRules.game:IsDemoMode() ~= 0) then
		if (open) then
			self.action=DOOR_OPEN;
		else
			self.action=DOOR_CLOSE;
		end
		self:Rotate(open, fwd);
	end	
end

As with the server function, we can see what the arguments will bring in Net.Expose: 2 booleans. The function itself is pretty simple, first we check if we are not a server (this is a common thing to do, we do not want the code to be executed twice on it) or if we are in demoplayback-mode. Then the action is set and self:Rotate() is called.

In the Door:Rotate() function we do the actual turning of the door, which is not in the scope of this article.


The final thing you might wonder is: When and how has initiated the SvRequestOpen function? A search in the file leads us to the Door:OnUsed() function.

Lua

function Door:OnUsed(user)	
	self.server:SvRequestOpen(user.id, self.action~=DOOR_OPEN);
end

The OnUsed function gets called by the lua system when using an entity (in this case, the door). So when a client (not necessarily you) uses a door, he will send a request to open the door, to the server.

You can see the syntax for client->server is even more simple than server->client. We just have to add a .server part and we’re good to go.

Предупреждение:
Do not forget Lua is case sensitive. If you use .Server instead of .server, it won't work.

Schematic form

ClientServer Door.jpg

Handling connecting clients

The final thing we have to consider is clients connecting in the middle of the game. Say client A opens a door. After a while client B connects. If client B runs up to the door he will see a closed door, since he didn’t get the 'open' message from the server, which was sent at the time client A opened the door.

To solve this there’s a function which gets called when a client connects: OnInitClient.

Lua

function Door.Server:OnInitClient(channelId)
	if (self.Properties.Rotation.fRange ~= 0) then					
		local open=self.action == DOOR_OPEN;
		self.onClient:ClRotate(channelId, open, self.fwd or false);
	end
 
	if (self.Properties.Slide.fRange ~= 0) then			
		local open=(self.action == DOOR_OPEN);
		self.onClient:ClSlide(channelId, open);
	end
end


When a client connects, this function gets called. It just sends the current state of the door to the client. So if client B now runs up to the door, he should see it’s open.