创建一种逻辑实体

From Valve Developer Community
< Zh
Jump to navigation Jump to search
English (en)Русский (ru)中文 (zh)Translate (Translate)


译者注:阅读之前拥有一定的C++知识将有助于你更容易理解本文的一些术语和内容。 逻辑实体是最简单的实体类型,因为它们是不可见的,并且它们存在就是为来自其他实体的输入(en)输出服务。比如说math_counter(en) 实体存储一个可以增加或减少的值,地图里的其他实体可以通过输入修改该值或者通过输出来获取该值。

在本教程中,我们将创建一种逻辑实体,负责存储一个值并且该值在每次收到正确的输入之后都会自增。当它存储的值达到我们设定好的数值之后,该实体将会触发一个输出动作。

创建源文件

本教程假设你使用的是Visual Studio或者Visual C++ Express。请参考编译器的选择(en)

Server项目内创建一个新的筛选器,随意起一个名字。然后添加一个新的.cpp文件到该筛选器内,并命名为"sdk_logicalentity"。该源文件将包含本教程所写的全部代码。

给每个实体一个.cpp文件可以减少编译负担,因为这样做可以使得编译器把你的代码分割成更加分散的代码片段,由于只有修改过的源文件需要重新编译,故而编译速度会加快。

包含文件

每个.cpp文件都是和工程内的其他文件独立开来的。为了避免这种情况,我们可以包含“头文件”,告诉编译器我们将使用工程内的哪个部分。

在这个极其简单的实体中,我们只需要包含一个文件:

#include "cbase.h"

cbase.h提供了对 Valve用于创建实体的基本命令集 的访问权,并且每个为Source引擎所编写的.cpp文件都必须包含这个文件。字母“c”是Source引擎中所使用的命名组成之一,它代表该文件的代码属于服务端(客户端一般是“c_”)。

声明类

这一小节有点长,但是你看完之后会对C++的运作方式有初步的了解。

几乎所有的过程都是由“对象”(objects)来操作的。“类”(class)就是软件生成对象时所使用的框架。创建一种新的实体就意味着创建一个新的类。

class CMyLogicalEntity : public CLogicalEntity
{
public:
	DECLARE_CLASS( CMyLogicalEntity, CLogicalEntity );
	... // 不要把这一行也复制进去!
};

这就是“类的声明”,告诉c++编译器我们想让这个类的对象存储什么内容,运行怎样的函数。

代码的第一行,我们用class指令声明了一个类。我们给它起名叫CMyLogicalEntity,冒号后面告诉编译器,这个类是从已有的类CLogicalEntity(en)继承的,因为之前包含了cbase.h,所以我们可以引用这个类。。

继承 的意思是这个类是建立在已有的类的基础上的。 CLogicalEntity 本身是比较简单的, 但是它所继承的类CBaseEntity(en)里,包含了大量的代码。由此可见如果没有继承的话,创建一个新的实体会把人折磨疯的。

class指令写完后,我们使用了一对花括号“{}”。花括号里面的代码就是一套组合;在本例子里,这个组合就是类本身。

接下来是修饰符”public:”, 在public: 下的成员,是类的公有成员,这个类的成员可以被其他类直接访问,而private: 下的是私有成员,只有类的内部能访问,类的外部无法访问的。你应该在有必要的情况下才建立公有成员:请参考维基百科:封装

第一条指令

我们现在可以设置指令了。DECLARE_CLASS是一个 宏定义,编译的时候它将自动转换成创建新实体所需要的变量和函数。

圆括号里的内容我们称之为”参数”。这些参数传递了必要的信息。你也许已经注意到参数之间使用逗号“,”分隔。

行尾的分号“;”说明这一条指令已经结束。因为编译器基本无视换行和空格,所以需要分号来告诉它这一行已经结束。

声明的结束 (暂时)

请不要输入... - 这只是代表这个地方还有其他的声明。我们先跳过这一步,用花括号和分号};结束这个类的声明。

Note.png注意:一般来说 } 后面不用加 ;,你只需要在声明类的时候这么做


声明一个 DATADESC,函数和构造函数

回到省略号的地方添加以下内容:

	DECLARE_DATADESC();

	//构造函数
	CMyLogicalEntity ()
	{
		m_nCounter = 0;
	}

	// Input function
	void InputTick( inputdata_t &inputData );

第一行是另一个宏,为等下的DATADESC(en)列表做准备。第二行是注释,//,让其他人了解你的代码(包括你自己,也许以后你会忘记)的含义。

第三行是类的”构造函数”。它是”函数”的一种:将一段经常需要使用的代码封装起来,在需要使用时可以直接调用。因为它有和类相同的名称,所以创建一个新的对象的时候这个构造函数就会被调用。在本例中,我们将m_nCounter初始化为0.变量是没有初始值的,如果在给它们赋值之前引用它们的话会发生不可预测的错误。

我们刚在它的整体“定义”了构造函数,但是大多数函数(包括更大的类的构造函数)放在一个类声明里面会使代码显得臃肿,所以,相反地,我们声明它并且留到后面才编写改.cpp文件的代码内容。

相应地,代码片段中的最后一行是一个函数声明,该函数将在接收到正确的输入时被调用,从而执行 给实体存储的值增加1 的功能。 void 表明该函数不会返回一个值,因为我们制作的输出会通过一条不同的路线来执行(构造函数是一种特殊情况,它不需要一个返回值类型,因为它们总是void类型的)

我们调用 InputTick 函数并且在圆括号内定义它需要传入的参数。对于我们现在的情况来说,我们只需要一个inputdata_t类型的名为&inputData的变量。This is information about input event that is automatically generated by the engine at run-time, including among other things its name, I/O arguments from the map (there won't be any), and the entity from which it originated.

&符号表示我们仅仅需要记录inputdata_t类型的实例在系统内存的地址,而不是把几乎所有关于输入事件的信息都复制过来。这个"pointer(en)"(指针)允许沃恩访问实例的数据而不需要把这些数据复制到当前函数所在的地方来。就像我们往往通过快捷方式打开文件而不是移动或者复制那个可执行文件到桌面再打开。

InputTick()后面没有接{}:它们会在我们在后面写函数代码时出现。我们需要用一个英文状态的分号来标识一个语句的结束。

私有声明

现在我们将声明私有成员。这些成员对类外面的任何地方而言是不可访问的,除非我们在一个公有函数中让这些私有成员变成可以访问的(但我们并不这样做)。

private:

	int	m_nThreshold;	// 数到多少才触发我们的输出
	int	m_nCounter;	// 内部计数器

	COutputEvent	m_OnThreshold;	// 当计数器到达阀值(我们设定的值)时的输出事件

前面的两个是变量。他们是电脑物理内存中可以填充一个值的篮子,用于计算,passed around, perhaps written to the disk, and emptied. They are "integer(en)" or "int" variables which can be used to store whole numbers like -1, 0, 1, 2, 3, and so on. They can't store words or characters however, nor numbers with decimal points. C++ is very strict about these kinds of things!

m_OnThreshold is an instance of COutputEvent, a class that Valve have already written for you in cbase.h. You'll use it in InputTick to pass a value through the I/O system when our conditions are met. It's actually a variable like int, but a custom one that doesn't come with the compiler. The only reason it isn't coloured blue as well is that Visual Studio doesn't recognise it.

A word about the names in this section. m_ means that the objects are members of the current class, while n means that the value is numeric (i for 'integer' could be used too). You don't have to follow these naming conventions, but they are very common.

Check back

That's the declaration finished. Check back over what you've written and make sure it's the same as the reference code(en). If it is, you're ready to move past that }; and into the body of the entity's code.

Linking the class to an entity name

We are now past the class declaration. In Visual Studio, you should be able to collapse that whole section by clicking on the second minus sign in the margin. Now, after the };, enter this:

LINK_ENTITY_TO_CLASS( my_logical_entity, CMyLogicalEntity );

This is another macro (you can tell from the capitals), this time linking the C++ class CMyLogicalEntity to the Source engine classname(en) my_logical_entity. Classnames are used by the I/O(en) system, by Hammer(en), and occasionally by programmers(en).

Note.png注意:Ordinarily you would need to wrap my_logical_entity in quote marks, making it a string(en), because it isn't 'defined'; the compiler won't understand what the term means and will generate an error. This particular macro does the wrapping for you, but as you will see in the next section, that isn't true for all of them!

Data description table

The data description table(en) is a series of macros that give Source metadata about the members of the class you mention in it. The comments in the code below should explain what each one does: DEFINE_FIELD, for instance, ensures that the m_nCounter value is stored in a saved game to prevent it from resetting to zero every time the game is reloaded. DEFINE_KEYFIELD on the next line does the same job, and also opens the value up for editing in Hammer.

This code has examples of the string values mentioned in the last section. Unlike the other commands, these are simply data that the compiler accepts, stores, and blindly exposes to the engine. You can put anything you like between the quote marks and the code will compile, but when it comes to actually instancing the entity Source will be confused unless everything is correct.

// Start of our data description for the class
BEGIN_DATADESC( CMyLogicalEntity )
	
	// For save/load
	DEFINE_FIELD( m_nCounter, FIELD_INTEGER ),

	// As above, and also links our member variable to a Hammer keyvalue
	DEFINE_KEYFIELD( m_nThreshold, FIELD_INTEGER, "threshold" ),

	// Links our input name from Hammer to our input member function
	DEFINE_INPUTFUNC( FIELD_VOID, "Tick", InputTick ),

	// Links our output member variable to the output name used by Hammer
	DEFINE_OUTPUT( m_OnThreshold, "OnThreshold" ),

END_DATADESC()

You'll have noticed that while our input has a function, our output is merely the COutputEvent variable we declared earlier. While an input requires processing when it arrives, and thus a function, an output will have been processed already by the time it leaves.

When writing your own datadesc tables, remember to include a comma after each DEFINE_* command.

Note.png注意:这里没有分号。 This is extremely unusual and only occurs because the data description is defined using macros!

创建Input函数

下载我们将要定义我们的“input”函数,当我们的逻辑实体收到通过IO系统传递的“tick”“input”时该函数将会执行。Notice the addition of CMyLogicalEntity:: to the function name, which explicitly links the function definition to our class.

//-----------------------------------------------------------------------------
// Purpose: Handle a tick input from another entity
//-----------------------------------------------------------------------------
void CMyLogicalEntity::InputTick( inputdata_t &inputData )
{
	// Increment our counter
	m_nCounter++;

	// See if we've met or crossed our threshold value
	if ( m_nCounter >= m_nThreshold )
	{
		// Fire an output event
		m_OnThreshold.FireOutput( inputData.pActivator, this );
		
		// Reset our counter
		m_nCounter = 0;
	}
}

Here's what's going on:

m_nCounter++
C++ shorthand for "increment by one". In full, it would be m_nCounter = m_nCounter + 1;. Another way of writing it is m_nCounter += 1;.
if ( m_nCounter >= m_nThreshold )
if statements need a true/false equation to evaluate. > means greater than, so combined with = we have an "operator" that says "greater than or equal to". If the value of m_nCounter is greater than or equal to the value of m_nThreshold, execute whatever is inside this command.
{ }
This is a "nested" or "child" bracket pair, within the functions own "parent" bracket pair. It groups all of the commands the if statement will execute if it evaluates to true. There can be as many levels of nesting as is needed, but be sure to keep track of them all!
m_OnThreshold.FireOutput
You probably pasted this in. Delete it and type it out in full, and note what happens when you reach the dot. You are calling a public function of the m_OnThreshold instance of the COutputEvent class, and Visual Studio helps you by providing a list of the possible options. Keep typing to filter it, or use up/down and return to select an item.
If we were to call the function we're currently writing from another class, we would type MyLogicalEntity1.InputTick(<arguments>), where MyLogicalEntity1 is the name of the instance we were targeting. (Which means that we would need to have a pointer to an instance of the class - we can't just access the template!)
( inputData.pActivator, this )
These are the arguments we are passing to the COutputEvent. They will be automatically generated by Source, and arrive at our function through the parameters we gave it between its ()s.
We're sending:
  1. The entity that started this whole chain off (in Source lingo the "activator").
  2. The entity that the call is being sent from, the "caller", which is "this" one.
These are needed for targetname keywords.
m_nCounter = 0
If we didn't reset our counter, the number would only keep on going up.

Congratulations, the entity is ready to be compiled - press F7 to start the process. If you receive any errors from Visual Studio, check the reference code(en)!

We can't use this entity directly from the engine however - if you've ever made a map, you'll know there's a lot of I/O information that we haven't made room for in this code. Thankfully our DATADESC table allows Source to plug this entity into the wider I/O system automatically; so the only thing left to do now is tell Hammer(en) about it.

在FGD中加入添加的逻辑实体

Hammer Options dialog

FGD(en)是一个定义了MOD的所有可用实体和实体用途的文本文件。这并不是一个完美的世界,Hammer不会自主读取MOD的代码,所以保持FGD文件版本为最新版本是非常重要的。

如果你还没有FGD(en)文件,在你的MOD目录下以<mod‘s name>.fgd的格式保存一个空的文本文件。然后打开Hammer(en)并依次点击Tools > Options。确保你在配置方案里已经选择了你的MOD,点击“Add”并找到刚才创建的FGD文件

FGD(en)s are a different topic, so this won't be covered in detail. Just paste the following code into your new file:

@include "base.fgd"

@PointClass base(Targetname) = my_logical_entity : "Tutorial logical entity."
[
	threshold(integer) : "Threshold" : 1 : "Threshold value."
	input Tick(void) : "Adds one tick to the entity's count."
	output OnThreshold(void) : "Threshold was hit."
]

And there you have it! 你的实体已经准备好投入工作当中去了。 See Hammer Entity Tool(en) for adding it to a map (place it in one of Valve's sample maps if you're struggling to create your own one), and Inputs and Outputs(en) for getting it doing things.

在Hammer中使用刚才创建的实体

要在你的MOD里使用该实体, 你必须在Hammer里将其放置在地图中(unless you use/insert it programmatically elsewhere)。假设你已经有了一张地图来测试并掌握了Hammer的基础知识; a very basic map made up of an empty room with an "info_player_start" in the middle works.

在这张地图里我们将会在地板上放置三样物品顺便加上一个手雷。玩家每捡起一次这些物品中的其中一个,逻辑实体的计数器都会增加1。当计数器计数达到3时 (该数值可以在Hammer里面设置),手雷会自爆。我们所要做的,就是插入objects/entities并定义它们各自的关系。很简单,难道不是吗?

Start by adding your entity from Hammer;选择实体工具(Shift+E), and on the right hand side, select "Entities" under "Categories" and "my_logical_entity" under "Objects".

Click any point in the map to choose the location of the entity to be created. Since this is a logical entity, it will be immaterial and invisible to the player, so its location is not that important; just put it where it won't bother you later.

Once the location has been pinpointed, right-click the selection and choose "Create Object" -- your logical entity will be added to the world.

你会立即想要做的是为这个逻辑实体一个名称;在任何视图里面右键然后选择“属性(记得先选择选择工具(Shift + S))。

属性菜单将会弹出来。在这里,你可以看到已经有两个属性值了:

实体名称 —— 地图中,my_logical_entity类型的特定实例的名称

上限 - 即C++类中的成员变量m_nThreshold, which we called "threshold" in the data description table and exported as "Threshold" in the .fgd file for Hammer to use. This is the count at which the entity will send out an "OnThreshold" signal/event.

我们就把该实体命名为"MyFirstCounter"吧。把"Threshold"属性设置为3。 (Click the variable name in the list on the left, enter the desired value in the field on the right).

一旦一切都设定好了,点击“apply”按钮并关闭对象属性窗口。

接下来,我们将会在地图上放置我们的物品。

首先是添加手雷。选择实体工具, 找到并选择"npc_grenade",在地图的任何一个地方创建该实体。打开它的对象属性窗口并给它命名为"MyBoobyTrap"。

现在将要添加一样玩家可以捡起来的物品。比如"item_ammo_pistol."。打开它的对象属性窗口并给它命名为"MyPistolAmmo"。然后点击对象属性窗口的第二个标签“Outputs”,添加新的“Output”。在下拉菜单中选择合适的项:“OnPlayerPickup”明显是我们想要的触发条件。在“target”里填写"MyFirstCounter," which will be incremented thanks to the "Tick" input function (to be selected in the "Via this Input dropdown"。这将会告诉游戏,让它命令计数器,每当玩家捡起一份手枪弹药时,计数器的计数增加1的"Tick"函数。

一旦设定好了,点击apply并关闭对象属性窗口。复制手枪弹药并粘贴两次或更多。它们所有的inputs/outputs将会被复制,并且它们的命名是一样的。

现在,回到逻辑实体的对象属性窗口, and modify its Outputs so that reaching the counter's Threshold value causes the grenade to "Ignite" (a predefined function of Valve's npc_grenade entity). Your output function will be "OnThreshold", the target will be "MyBoobyTrap" and the input you want is "Ignite". 你也可以设置延迟时间,如果你想的话。This will cause the Valve-defined Ignite function of the grenade to run as soon as the counter hits its threshold (OnThreshold event generated).

现在一切就绪。点击File > Run Map或者按下F9 (首先要确保你已经编译了项目),然后编译地图。在地图里面,你能够捡起手枪的弹药。一旦你捡起第三样物品,手雷将会爆炸。(如果你的武器弹药是满的话,随便射击消耗弹药来捡起弹药)。

扩展阅读