topameng's profileQuake3 启示录PhotosBlogListsMore Tools Help

Blog


    June 26

    quake3 脚本

    首先,为了记住你上一次使用的枪,需要设定一个变量,这里用prevweapon,并且把它的值的设定绑定在每把枪上。譬如你用5做炮:
    bind 5 "weapon 5;set prevweapon weapon 5"
    然后是具体动作的脚本了。先设定一个摁下一个键的动作。
    set sniper_hold "weapon 7;+zoom"
    weapon 7是RG,+zoom是放大,呵呵,有点废话。之后是放开这个键的动作。
    set sniper_release "+attack;wait;-attack;-zoom;vstr prevweapon"
    这句话里,+attack是攻击,-attack是停止攻击。为什么非得这么写呢?因为在Quake3里,和Zoom,Speed一样,attack分两个状态,当你把它独自绑定在一个键上,摁下代表+attack,松开就是-attack,在这里,由于不是这样的一个键上的单独绑定,+attack就会使“小人儿”处于攻击状态,不停的开枪。所以之后要加一个-attack。为了防止-attack在+attack还没发生做用的时候执行,所以在中间加了一个wait。其实还可以加更多。如果你本来就开了一枪,那么+attack这个状态将在装子弹不能攻击的时候发生作用,而-attack发生作用的时候,根本就没有开枪。根据自己的情况,可以将其改为wait 20之类,这相当于你写20个wait:)。

    -zoom没什么说的。接下来的一句,首先,你要明白set prevweapon weapon 5这一句并不是说prevweapon就是等于weapon 5的作用了。prevweapon是一个变量,而不是命令或者代命令。要想使得其中储存的命令得以执行,就需要vstr这个命令。vstr,说白了就是执行变量里的命令的命令。最开始我写脚本的时候,就总忘了vstr呵呵。现在vstr了prevweapon,由于你上次用的枪是RL,当你摁下q的时候,prevweapon里的值是weapon 5,所以在放开这个sniper用的键的最后,它将换回weapon 5,RL。

    接下来我们要把它们真正绑定在一个键上了,我这里用MOUSE3。我们要把sniper_hold绑在摁下去、把sniper_release绑在松开来上。
    这里有两种方法,其中一种已经过时了,而且特别容易出毛病,我在这里就不说了。另外一种是osp的方法,其实osp文档里就有说明的。我在这里用这个实例给初学者再说一下。
     
    bind Mouse3 "+vstr sniper_hold sniper_release"
    这个是osp模式里最好的实现方式了,其他的方式也需要osp、很难使而且可以绑定的键有限。毫不夸张的说,+vstr这个命令是osp发明以来对脚本的最大贡献!举个例子,RB上次发布cfg大家都用了,如果大概意思不改动,实现换电枪无抖动也需要用到+vstr。我认为,只要你想得到,很多地方+vstr已经可以代替以前很多vstr来实现的脚本,一方面它功能强大,可以做出很炫的功能;另一方面,用+vstr可以将一些现存的脚本简化,使得脚本有更大的可读性。

    没有安装 osp mod , 对于quake3.1.32b 的代码可以使用如下的脚本:
    set sniper_hold "weapon 7;+zoom;bind Mouse3 vstr sniper_release"
    set sniper_release "+attack;wait ;-attack;-zoom;vstr prevweapon;bind Mouse3 vstr sniper_hold"
    bind Mouse3 "vstr sniper_hold"
     
    保存脚本到 myscript.cfg 中,进入关卡执行 exec myscript.cfg
    或者在 q3config.cfg 中绑定一个键来加载这个文件
     
    由此可以看到 cvar 控制台命令的强大。可以组合各种命令来简化操作。很类似魔兽世界的宏,只是缺少了条件判断。而魔兽的宏更类似脚本,写起来似乎有些长而不实用
     
    June 24

    CVars、Commands and vm Communication

    One of the most common questions I'm asked through e-mail is "How do I get the client to do something from the server code?"... or variations on the theme. The issue is made more complicated by the fact that client, server, and user interface each have their own methods and restrictions.

    This article aims to de-mystify the methods you can use to pass information between the three Virtual Machines (VM's). There are several complementary methods, and I'll outline the advantages and problems with each.

    We'll start with a desciption of the client/server/interface model in Quake3. Understanding the relationship between these components is at the core of the need to communicate between them. Some of the restrictions in the system are also clarified.

    We'll then move onto the main methods: the setting of variables, also called Cvars, and how they're related to configuration strings maintained by the server. We'll then look at the sending of console commands, and finish off by seeing how we can drive the server scripting engine.

    I've tried to write each main section as an overview and introduction to the sub-sections that follow. As the ideas and definitions can get confusing IT IS IMPORTANT THAT YOU READ AND UNDERSTAND EACH OF THESE OVERVIEWS before proceeding with the more specialized descriptions.

     

     

    1. Server, client, and user interface

    Whether you're compiling binaries or bytecode for the platform independant VMs, it's clear that the game is split into three parts: server (qagame), client (cgame), and user interface (ui). Each carries responsibility for implementing a separate part of the game. What isn't so obvious, and is often forgotten, is that there's a fourth part: the executable that runs the VMs.

    The server and client complement each other and carry just about everything needed to play the game. The server controls and arbitrates the game: it decides where a player is on the map, whether they've been hit by that rocket, and where the momentum kick sends their corpse. The final word in what happens in the game, the server makes sure that the game world remains consistant.

    On the other hand, the client has only a limited view of the world. It's given information by the server that it needs to present this localized view to the player... and little else.

    The client is responsible for drawing the screen, playing the sounds, and adding all those little meaty gibs. It also includes an understanding of the game physics so it can predict motion and make gameplay smoother over an Internet connection, but it doesn't settle the final position of the other players (or yourself).

    To make this clear with an example: the server concerns itself over who has the quad, applying the damage multiplier, and settling disputes when two players try to pick it up at the same time. The client is told who has the quad, plays announcements and damage sfx, and draws that blue glow that warns "something dangerous comes this way!"

    It's possible for the client and server to be on separate machines, this is how an Internet game is played. Communication has to occur over a time delay determined by the connection quality and latency (ping), so there will be a delay between a command being sent and received. There's nothing that can be done about that.

    The client and user interface are always on your local machine. Think of them as connected to your graphics and sound cards, and you won't go far wrong. Servers don't care about these things directly, the closest they get is sending out a command saying "play this sound" or "do this action".

    The user interface covers all of the menus and pages of controls used to set up personal preferences and game parameters. It is the user interface that draws the menu when you hit the Escape key while playing a game. Anything the behaves like a dialog box or page of controls is drawn (and interacted with) in the user interface.

    Finally, it's important to recognize that there's a fourth part to all this, hidden away from prying eyes because the source code isn't available: the binary executable. This runs the VMs required to play the game, manages the communication between the VMs, controls the network connection if there is one, and handles all the OpenGL drawing of the graphics.

    Any communication between VMs is going to have to pass through the binary executable. It's the only way.

     

    2. Cvars and configuration strings - passive changes of state

    Console variables (Cvars) store the current state of the game system in a way that can be accessed through the console while playing, and directly in the game code. Many Cvars are read only: you can't change their value. Some can only be modified when cheats are enabled. Most are saved for the next time you play Q3.

    As a variable a Cvar isn't a command that must be obeyed immediately. What usually happens is that your code will check the value the next time it needs to use it, and adjusts its behaviour depending on the value it finds stored.

    This is helped by the way a Cvar is updated. A copy of the Cvar is maintained in the source code, this can be read and used in any part of that source code module. This local copy is updated at regular intervals so you have both consistancy of value, and an (almost) up to date value.

    Note that we are accessing a "copy" in the source code, the original or "master" of the variable is stored and maintained by the executable. We'll talk about this in more detail when we look at how and when the Cvar copy is updated (section 2.2).

     

    2.1 Where to place your Cvar

    A Cvar is created in the code as a data structure. You need to provide a default initialization value, and flags that control access and if it will be saved in q3config.cfg for permanent storage.

    The Cvar that you access in your code is placed in either game/g_main.c, cgame/cg_main.c, or ui/ui_main.c. You need to choose the most appropriate place: there is no point in putting a Cvar that controls behaviour of the server in the user interface code!

    There are benefits to putting the Cvar in the correct place, described in section 2.2.

    You can place a Cvar in another source code file, but you won't get the benefit of automatic initialization and updating.

     

    Example - Setting up a Cvar

    We'll take a quick look at how you initialize a Cvar and make it available for the rest of your code to use. This example looks at the cg_drawFPS variable in the client that draws the frame rate counter on the screen.

    Although taken from cgame code it applies equally well to the ui in ui_main.c. Its done slightly differently in game, and we'll look at that in a moment.

    Taken from cg_main.c:

    vmCvar_t	cg_drawFPS;
    
    
    cvarTable_t		cvarTable[] = {
    	// earlier Cvars snipped
    
    	{ &cg_drawFPS, "cg_drawFPS", "0", CVAR_ARCHIVE  },
    
    	// following Cvars snipped
    };
    

    A variable of type vmCvar_t is created to store the current value. To take advantage of the automatic updating of the Cvar we need a reference in the array cvarTable[]. This reference takes four components, from left to right they are:

    &cg_drawFPS
    A pointer to the vmCvar_t that stores the Cvar value

    "cg_drawFPS"
    The name of the Cvar as typed into the console

    "0"
    The default initialization for the variable, this can be any string

    CVAR_ARCHIVE
    A flag controlling the behaviour of the variable

    You can find more of the CVAR_* flags in q_shared.h, they're quite well documented there.

    Finally, so that the Cvar can be accessed easily from within related source code files, you need to add a reference to the vmCvar_t into the local header file. For cgame this is cg_local.h, while the ui and game have their equivalent in ui_local.h and g_local.h respectively.

    For our example you'll find this in cg_local.h:

    extern	vmCvar_t		cg_drawFPS;
    

     

    I mentioned earlier that the server was slightly different. The change is in how the Cvar is linked into the list for automatic initialization. Here's what a typical Cvar looks like in g_main.c:

    vmCvar_t	g_gametype;
    
    cvarTable_t		gameCvarTable[] = {
    	// earlier Cvars snipped
    
    	// latched vars
    	{ &g_gametype, "g_gametype", "0", CVAR_SERVERINFO | CVAR_LATCH, 0, qfalse  },
    
    	// following Cvars snipped
    };
    

    The first four entries are identical to their cgame and ui counterparts, but there are two new entries at the end:

    0
    Always initialized as zero, this is the number of times the value of the variable has changed.

    qfalse
    If set to qtrue then an announcement (text message) is sent to all clients that this Cvar has been changed.

     

    2.2 When Cvars are updated

    Even though we can access the Cvars from within the source code, these are not the "master" or "primary" record of the Cvar value. This might sound surprising, but arises from the need to communicate these values between VMs.

    The authoritative value of each Cvar is stored within the executable running the client or server, and the Cvars within the source code are updated to reflect new values on a regular basis. This update might be slightly delayed, but ensures consistancy in use. More on this in a moment.

    In the situation where the client and server are running on separate machines, the client still has access to most of the server Cvars. They can only be read by the client, and not modified.

    The reverse situation where the server accesses a clients Cvars is complicated by the fact that the server can have multiple clients. The mechanism by which a server can access the client is covered in detail in section 2.3.3.

    The copy of the Cvars stored in each of the *_main.c is updated on a regular basis at a "convenient" point in the game play cycle. This point is usually slightly delayed from the time when it was modified in the primary record.

    For the client this update occurs just before the next screen is drawn by CG_DrawActiveFrame() in cg_view.c. This makes sense when you realize that these local copies can be accessed through the vmCvar_t data structure without reference to the primary record. For the entire frame that is about to be drawn you will get a consistant value for the Cvar if you use the value stored in a vmCvar_t variable structure.

    This is important. Go back, read and understand that last paragraph again. Even if the Cvar changes in the primary record while the client is rendering the screen, you get a consistant value to work with while drawing the entire screen. If you always grab the "most up to date value" with a trap_* call then you might get rendering errors for that frame. Also, if you change the value of a Cvar through a trap_* call then you won't see the new value until the next update.

    For the user interface this Cvar update occurs every time the screen is drawn in UI_Refresh() in ui_atoms.c. For the server code the update is triggered in G_RunFrame() in g_main.c, when the server updates the postion of each entity in the world.

    As you can see, the Cvars are updated frequently, but not so frequently as to disrupt a frame of activity. The benefit of these local copies is twofold: constancy and speed of access to a local data structure.

     

    2.3 Reading and setting a Cvar

    With the Cvar created, we need to look at how the value within it can be accessed and changed. The user interface and client behave in a similar way, while there are special considerations for the server.

    By using a trap_* function to set the variable the primary record is updated correctly. You MUST NOT change the value of the local copy of the Cvar - it won't be moved to the primary record, and will only be over-written at the next update. If the Cvar is archived in q3config.cfg then the new value won't be saved either.

    A Cvar data structure looks like this:

    typedef struct {
    	cvarHandle_t	handle;
    	int		modificationCount;
    	float		value;
    	int		integer;
    	char		string[MAX_CVAR_VALUE_STRING];
    } vmCvar_t;
    

    You can read the value of the Cvar by accessing the value, integer, or string fields. This is only possible when the Cvar is stored within the VM you are coding in. In this case it is the optimal method of access. For a Cvar from another VM refer to the methods described below.

    Although, in principle, you can read and modify Cvars from other VMs, the most cautious approach is to treat Cvars from a different VM as read-only. You might need to know a clients preferences to help make the server run more efficiently, but indiscriminantly changing a clients preferences from the server is anti-social.

     

    2.3.1 Cvars in the ui

    The method used to set the value of the Cvar happens to be the same in all three modules, treating the Cvar as a string value that can be changed:

    void trap_Cvar_Set( const char *var_name, const char *value );
    

    var_name
    The name of the Cvar as typed on the console

    value
    The new string that replaces the old one

    The ui also has an additional method that sets the string using a float value rather than a string:

    void trap_Cvar_SetValue( const char *var_name, float value );
    

     

    Retrieving the value of the string is done through the complementary methods:

    void trap_Cvar_VariableStringBuffer(const char *var_name, char *buffer, int bufsize );
    float trap_Cvar_VariableValue( const char *var_name );
    

    var_name
    The name of the Cvar as typed in the console

    buffer
    Pointer to the char array to store the current string value in

    bufsize
    The size of the buffer for storing the string, prevents memory overflow and corruption

     

    Special methods - Configuration Strings

    These are described in great detail in the discussion of client Cvars (section 2.3.2, Special methods - Configuration strings). They represent a method that allows the client and user interface to read a set of strings that the server has set: these strings being common to (and used by) all connected clients.

    You can access these strings through a call to the following function:

    int trap_GetConfigString( int index, char* buff, int buffsize );
    

    index
    type of string you want to retrieve

    buff
    pointer to the buffer to store the configuration string into

    buffsize
    size of the buff array, I'd recommend using MAX_INFO_STRING

    You can't change the value of the configuration strings from the user interface. Check below in the client and server sections on how to process and extract information from them.

    (The truth be told: I didn't find out that the user interface could access these strings until after I'd written the client description of them. I'm too lazy to re-write that description for here :)

     

    2.3.2 Cvars in cgame

    Setting the value of a Cvar in cgame occurs through the same function call as in ui:

    void trap_Cvar_Set( const char *var_name, const char *value );
    

    You can read a Cvar's string value by making a call into the following function:

    void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize );
    

    and this behaves in the same way as the identically named function in the ui module.

     

    Special methods - Configuration Strings

    For quick access to important Cvars stored in the server, there is a flexible and powerful method available for use. Strictly speaking not all of variables passed through these configuration strings are Cvars, neither are all the server Cvars accessible through this method. However just about all the important ones you'll need in the client are available.

    These configuration strings are set and maintained by the server, and replicated to each client when a connection is first made. When the server updates any of these configuration strings, each connected client gets an updated version.

    These configuration strings are also used to pass information about other clients connected to a server. This information is limited (but useful), and includes things like player name, choice of model, and railgun trail colour. Take a look in ClientUserinfoChanged() in game/g_client.c to see how this string is constructed by the server, and in CG_NewClientInfo() in cgame/cg_players.c for parsing of this info in the client.

    The most up to date version of the configuration strings known to the client can be read through a call to:

    const char *CG_ConfigString( int index );
    

    index
    takes a value related to the type of string(s) you want to retrieve

    Most of the values that index can take are documented in bg_public.h as the CS_* family of flags. You can extend these values by adding new CS_* flags, it looks like there are gaps before CS_MODELS available for expansion, and about 350 free slots after the current value of CS_MAX.

    When a change is made to one of these configuration strings, the client gets notification though CG_ConfigStringModified() in cg_servercmds.c. You can add or extend any processing of the changed configuration string here, or examine how existing strings are parsed to gain further understanding.

    Setting these strings is described in the next section about the server. You can also add strings using two of the existing values of index provided (described below).

    You shouldn't move any of the CS_* values around because comments about the sound values in the source code suggests optimizations within the executable for sending this information across the network. As you can't change the executable, don't mess with the CS_* flags too much!

    The pointer returned by CG_ConfigString() can currently take two forms, and you need to find out which by examining how these CS_* returned strings are used in the source code. Take a look in CG_ConfigStringModified() in cg_servercmds.c or do a GREP through the source code to find out where else they're used or set.

    The first form is a simple string that contains the current value. An index value of CS_MOTD, CS_WARMUP, and CS_MESSAGE behave in this way. You can then process them with an atoi() if they're numerical, or treat them as strings.

    The second form is a list of variable names and value pairs. These can be queried through the provided method:

    char *Info_ValueForKey( const char *string, const char *key );
    

    string
    the pointer returned by CG_ConfigString()

    key
    the name of the Cvar string found on the server, and located in string

    You'll find that the two most powerful values of index are CS_SYSTEMINFO, and CS_SERVERINFO. They contain the server Cvars that are described respectively by the CVAR_SYSTEMINFO and CVAR_SERVERINFO flags in g_main.c.

    You can add your own Cvars in the server using these flags, they'll then be sent to each client when they're updated by the server. Use CVAR_SERVERINFO to describe a string that characterizes the game playing on the server (gametype, fraglimit, mapname etc.). Typically these are the values that a program like GameSpy-3D queries and displays. For the remaining Cvars that describe the low-level state of the sytem, CVAR_SYSTEMINFO should be used (g_syncronousClients is an example).

    Don't put frequently changing values into the CVAR_SERVERINFO or CVAR_SYSTEMINFO flags as the entire string is resent for the change of one variable. Very network inefficient.

    How the server sets these configuration strings is described later in this article.

     

    Example - Using Info_ValueForKey()

    Taken from CG_DrawInformation(void) in cg_info.c, we'll get the short name of the map (q3dm10, q3tourney6 etc.) from CS_SERVERINFO and load an image of the level ready for drawing.

    const char	*s;
    const char	*info;
    qhandle_t	levelshot;
    
    info = CG_ConfigString( CS_SERVERINFO );
    
    s = Info_ValueForKey( info, "mapname" );
    levelshot = trap_R_RegisterShaderNoMip( va( "levelshots/%s.tga", s ) );
    if ( !levelshot ) {
    	levelshot = trap_R_RegisterShaderNoMip( "menu/art/unknownmap" );
    }
    

    Initialization of mapname is slightly unusual and occurs as part of the bot AI in ai_main.c instead of g_main.c. Nevertheless, it has been correctly registered and flagged as CVAR_SERVERINFO.

     

    2.3.3 Cvars in game

    Just as in ui and cgame, we have to set the new value of a Cvar through this function:

    void trap_Cvar_Set( const char *var_name, const char *value );
    

    There are two methods available for reading the value of a Cvar. These are

    int trap_Cvar_VariableIntegerValue( const char *var_name );
    void trap_Cvar_VariableStringBuffer( const char *var_name, char *buffer, int bufsize );
    

    The former takes a Cvar name and returns its integer value, and the latter is used in the way described in the ui (section 2.3.1), above.

     

    Special methods - accessing Cvars set in a client

    Of particular importance to the server is the ability to obtain some of the client parameters. Some of these parameters might be used to optimize the servers behaviour towards that client, others repackaged and sent on to the remaining clients for accurate representation.

    For a client Cvar to be forwarded to the server when it is changed, it must be described by the CVAR_USERINFO flag. The only example of this is the teamoverlay Cvar in the client: it's also a CVAR_ROM so it can only be changed from within the source code, not on the console. This Cvar controls whether the server sends bandwith sucking stats during team games. Other critical Cvars appear to be flagged as CVAR_USERINFO by the executable directly.

    When a client changes one of its Cvars, the updated value is sent to the server and notification arrives at ClientUserinfoChanged() in g_client.c. The information can be validated (if required), modified (if absolutely necessary), or repackaged as a configuration string and sent on for other clients to use.

    The client configuration is read and set through the following functions:

    void trap_GetUserinfo( int num, char *buffer, int bufferSize );
    void trap_SetUserinfo( int num, const char *buffer );
    

    num
    the index value identifying the client

    buffer
    pointer to the buffer string for storing/setting the user info. I'd recommend char buffer[MAX_INFO_STRING]

    bufferSize
    size of the buffer string, preventing overflow on read

    The client index is just the array index of the global server array level.clients that points to the gclient_t data structure representing that player. Look in ClientConnect() in g_client.c to see this. To obtain the client index given a gentity_t* use the following code (which relies on the properties of C pointer subtraction):

    int clientNum;
    gclient_t* client;
    gentity_t* ent;	// must point to a valid entity structure
    
    client = ent->client;	// must be non-zero for an entity that's a client
    clientNum = client - level.clients;
    

    Just like the configuration strings described earlier, you can access the value pairs using Info_ValueForKey(). This example checks whether cg_predictItems is enabled in the client. It's derived from ClientUserinfoChanged() in g_client.c:

    int clientNum;	// set in the argument to ClientUserinfoChanged()
    gentity_t *ent;
    char	*s;
    gclient_t	*client;
    char	userinfo[MAX_INFO_STRING];
    
    ent = g_entities + clientNum;
    client = ent->client;
    
    trap_GetUserinfo( clientNum, userinfo, sizeof( userinfo ) );
    
    // check the item prediction
    s = Info_ValueForKey( userinfo, "cg_predictItems" );
    if ( !atoi( s ) ) {
    	client->pers.predictItemPickup = qfalse;
    } else {
    	client->pers.predictItemPickup = qtrue;
    }
    

     

    In order to use the special parsing function Info_ValueForKey() when constructing your own configuration strings, you need to know how to build these strings.

    There's a working example in ClientUserinfoChanged() in g_client.c, but we'll take a trivial one for clarity. This is how the string would be constructed in the C source (note that the double slash as used in the source code is reduced to a single slash by the compiler).

    "string_one_key\\string_one_value\\string_two_key\\string_two_value"
    

    Values are paired up: each having a unique identifying name (key) and an associated value. The '\\' character is used to separate the values, and you shouldn't pass this character as part of a key name or value (this will prevent Info_ValueForKey() from working), nor a quote ("), or a semi-colon (;). I got these limitations from Info_Validate() in q_shared.c, but be wary of using non-standard characters, and never use a string termination '\0'.

    In this example string_one_key and string_two_key are the possible arguments to Info_ValueForKey(), and string_one_value and string_two_value will be returned.

    The key values shouldn't need to be Cvars, they're just a means of identifying the associated value uniquely. If you are only ever going to send a single string value, then you don't need to use this key/value pair system.

    You will almost certainly end up constructing these strings using the va() variable argument substitution strings, I deliberately avoided them to reduce the complexity of this explanation.

     

    Although there are no examples of the server over-riding the client settings using trap_SetUserinfo(), presumable you just need to send the variables you want changed using the key/value pairing described above. The best place for the server to do this is in ClientUserinfoChanged() in g_client.c

     

    Special methods - configuration strings in the server

    As already described earlier from the point of view of the client, configuration strings contain global information that is maintained by the server. It is transmitted to all the clients connected to the server, and updated when changes are made by the server. The client can't modify these strings, but can (and does) make use of the information.

    A configuration string is read or changed using these functions:

    void trap_GetConfigstring( int num, char *buffer, int bufferSize );
    void trap_SetConfigstring( int num, const char *string );
    

    num
    the index of the configuration string

    buffer
    destination buffer for copying the config stirng to. Use a char array of size MAX_INFO_STRING

    bufferSize
    the size of buffer, prevents overflow

    string
    new value for the configuration string

    Each configuration string is described by an index CS_*, a list of these values can be found in bg_public.h. Although there's space for MAX_CONFIGSTRINGS (1024) values, the smallest amount of change that can be transmitted across a network connection is a single string. This is a network efficient way of transmitting state changes in the server, and other global variables maintained by the server.

    Two special config strings are CS_SYSTEMINFO and CS_SERVERINFO. Described in more detail in the client section above, the Cvars that belong to this group are automatically updated when they're modified by the server using trap_Cvar_Set(). Try to avoid putting frequently changing values in here because it's almost certain that all Cvar strings in this group are transmitted in one go, and this is network inefficient. You can create your own specific CS_* flags and groups instead (like CS_FLAGSTATUS).

    For an example we'll take a look at how the server updates CS_FLAGSTATUS, this code is from g_team.c:

    void Team_SetFlagStatus( int team, flagStatus_t status )
    {
    	qboolean modified = qfalse;
    
    	switch (team) {
    	case TEAM_RED :
    		if ( teamgame.redStatus != status ) {
    			teamgame.redStatus = status;
    			modified = qtrue;
    		}
    		break;
    	case TEAM_BLUE :
    		if ( teamgame.blueStatus != status ) {
    			teamgame.blueStatus = status;
    			modified = qtrue;
    		}
    		break;
    	}
    
    	if (modified) {
    		char st[4];
    
    		st[0] = '0' + (int)teamgame.redStatus;
    		st[1] = '0' + (int)teamgame.blueStatus;
    		st[2] = 0;
    
    		trap_SetConfigstring( CS_FLAGSTATUS, st );
    	}
    }
    

    The flag status is sent as a two digit pair, one each for the red and blue flags. Sending the values as a pair of numbers is a limitation of strings: we must avoid sending anything that might prematurely terminate the string (a char value of '\0'), so its encoded based on the ASCII value for the character '0'.

    This code fragment shows how its decoded in the client, in the function CG_ConfigStringModified() in cg_servercmds.c:

    } else if ( num == CS_FLAGSTATUS ) {
    	// format is rb where its red/blue, 0 is at base, 1 is taken, 2 is dropped
    	cgs.redflag = str[0] - '0';
    	cgs.blueflag = str[1] - '0';
    }
    

    and you can see that the binary value of zero, if stored directly in str[0], would have prevented the rest of the string from being transmitted. The status of the blue flag would then be unknown.

     

    2.4 Some special Cvars

    There are some Cvars that have slightly unusual properties. There are only a few of these Cvars, and I'll only touch on them briefly.

    One reason for these special Cvars is so that one VM can control or influence another in a transparent manner. The three Cvars that fall into this category are sv_running, cl_paused, and g_syncronousClients.

     

    If sv_running is set then the server is running on your local machine. This is important as its presence enables some menu items in the ui that can be used to influence the local server.

    You can also check for this in the client code by accessing cgs.localServer (zero if we're connected to a remote server), it's set in the following manner:

    char var[MAX_TOKEN_CHARS];
    trap_Cvar_VariableStringBuffer( "sv_running", var, sizeof( var ) );
    cgs.localServer = atoi( var );
    

     

    cl_paused is used to indicate to the client that the user interface has drawn a menu on screen while the client is running. Setting this variable to a value of 1 (one) will pause the game if the server is local. This appears to be managed by the executable, stopping the server and client VM's temporarily, rather than in any of the VM code itself.

     

    The third variable is g_syncronousClients, this is set if the server allows demo recording, and it also plays a part in client side prediction. It is created in the server VM, and replicated to the client VM via the CS_SYSTEMINFO configuration string. The client can't modify this value, but it can be read in and acted upon.

    Note that the creation in the client of the vmCvar_t structure doesn't have any CVAR_* flags associated with it, this is because the server stored value is the one with the CVAR_ARCHIVE flag set. In other words, the server has control over the value, while the client contains only a reference.

     

    Another variable, apparently set by the executable, is bot_enable. If this is set to zero then bots aren't loaded onto the system. There is no corresponding copy stored in a VM, it appears to be managed by the executable running the server (but is available to the client). Again, some menu options depend on this value to determine whether they are enabled or not.

     

    3. Console commands - do this, now!

    With the large amount of ground covered by the Cvars and server configuration strings, you're going to find console commands has less content. Unfortunately the picture is made difficult to follow because of the possibility of confusion between the client and server consoles. In addition the server can issue commands to the client, in a way that looks like the console commands, but is in fact separate. I've tried to give the best description of these as I possibly can below.

    Client console commands, as their name suggests, can be typed in from the drop down console. There is also a way of executing these commands from within the source code. Whether the command originates from the console or code is irrelevant when it's acted upon. This means that these commands can't be hidden from the user in the same way as configuration strings can.

    A client console command can be handled in either the user interface, client, or server parts of the code. This makes it a very useful way of asking another VM to do something for you (for example: displaying a menu system using the user interface). We'll look at the client console in section 3.1.

    The server console is another matter. These commands control how the server behaves and they can only be accessed when the server is running on the same machine, or a particular client has the administrator privilige to change the setup of the server remotely. Some commands that fall into this category are addip and addbot, and we'll cover the server console in section 3.2.

    What is potentially confusing is that when the client and server are running on the same machine, both the client and server console commands are entered in the same way. The typing of commands through the drop down console is, at best, a confusing use of terminology.

    Apply this rule of thumb: A command typed in by the player is a client console command if is expected to behave the same whether the server is local (running on the same machine) or remote (on a LAN or across the Internet).

     

    3.1 Client console commands

    Once a console command has been recognized (Cvars are filtered out first), it arrives at the handler for each of the VM modules. For the user interface this handling function is UI_ConsoleCommand() in ui_atoms.c, the client has CG_ConsoleCommand() in cg_consolecmds.c, while the server uses ClientCommand() in g_cmds.c.

    Some pre-processing has already been done for you, and you can get at arguments for the command by using trap_Argv(int pos) (the argument at a given position), and trap_Argc() (the total number of arguments). The user interface and client have "wrappered" versions that can also be accessed through UI_Argv() and CG_Argv(). You'll find that Argv(0) is always the command string itself.

     

    3.1.1 Client console commands in the user interface

    There's no rocket science to adding a console command to the user interface: just add a Q_stricmp( cmd , "yourcmd")==0 comparison into UI_ConsoleCommand() and route it to you handling function.

    The most obvious use of a command in the user interface is to pull up a menu for the user to interact with, and there's an obvious example in the ui_teamOrders command. One thing to be cautious of if you're displaying a menu: you'll need to protect against repeatedly adding new menus until the menu stack is used up.

    Surprisingly there's no way of sending out a console command from the user interface, it has to go through Cvars or server scripting (see section 4). I would guess that's because it's possible for the user interface to be running when the client and server code aren't. Any Cvars that are important and stored outside the user interface are latched anyway - so any updated value is still passed correctly.

    You can get some information about the state of the client through a call to trap_GetClientState(uiClientState_t*). This data structure is described in ui_public.h, and includes (amongst other things) access to the connection state of the client. This might influence how you draw the menu: in a connected game you'll want to leave the background "transparent" so the player can see the game screen. This code fragment taken from UI_ConfirmMenu() in ui_confirm.c shows how this is done:

    uiClientState_t	cstate;
    
    trap_GetClientState( &cstate );
    if ( cstate.connState >= CA_CONNECTED ) {
    	s_confirm.menu.fullscreen = qfalse;	// no black background
    }
    else {
    	s_confirm.menu.fullscreen = qtrue;
    }
    

    See my articles on the user interface menu for more information on how and why ths works.

     

    Special treatment - The In Game (ESC) Menu

    When you press the Escape key while playing a game you get the In Game Menu that allows you to change teams, add bots, change game settings etc. This menu is unusual: it doesn't use a console command to activate it.

    The ESC key is recognized in the executable and passed into the user interface VM through vmMain() in ui_main.c. Although several menus are created by this method, and all pass through UI_SetActiveMenu() in ui_atoms.c I haven't been able to find a way for the user to extend this and add their own menus.

    The good news is that you can use console commands to create your own menus. Just make sure that the commands in UI_SetActiveMenu() don't overwrite your menu, and that UIMENU_NONE is always honoured.

     

    3.1.2 Client console commands in cgame

    Once again, adding a client console command is straight forward. It's made even easier by the use of an array of console commands and associated function handlers in cg_consolecmds.c.

    As the code fragment below shows, just add an new entry into the the commands[] array, with your command name and your handling function. This handling function must be of type void function_name(void) for the array to work.

    typedef struct {
    	char	*cmd;
    	void	(*function)(void);
    } consoleCommand_t;
    
    static consoleCommand_t	commands[] = {
    	{ "testgun", CG_TestGun_f },
    	{ "testmodel", CG_TestModel_f },
    
    	// code snipped
    
    	{ "tcmd", CG_TargetCommand_f },
    	{ "loaddefered", CG_LoadDeferredPlayers }	// spelled wrong, but not changing for demo...
    };
    

     

    You can send out a client command to another VM by calling trap_SendConsoleCommand(const char* cmd_string). You can add arguments with additional information so long as you separate each of them with a space. When they arrive at the VM that handles them they'll already be processed so each argument is easily accessible.

    There is also a similarly named (and possibly confusing) function called trap_SendClientCommand(const char* cmd_string). This sends the command string directly to the server, without a chance being given to the user interface to act upon it. Use it as part of exclusively client-server issues.

    When the command string arrives at the server, whether it came from trap_SendConsoleCommand() or trap_SendClientCommand(), it is handled in ClientCommand() in g_cmds.c.

    This example shows how the tell_target client console command is broken down into a single tell command. Ultimately this will be processed in the server for distribution to the victim under the crosshair.

    static void CG_TellTarget_f( void ) {
    	int		clientNum;
    	char	command[128];
    	char	message[128];
    
    	clientNum = CG_CrosshairPlayer();
    	if ( clientNum == -1 ) {
    		return;
    	}
    
    	trap_Args( message, 128 );
    	Com_sprintf( command, 128, "tell %i %s", clientNum, message );
    	trap_SendClientCommand( command );
    }
    

     

    If you create a command in the server and you want TAB completion to work in the client, then you should also register the server command in the client. This is done in CG_InitConsoleCommands() in cg_consolecmds.c by adding another trap_AddCommand() to the list.

    Why do it this way? Well if the server is running on another machine then you don't want TAB completion to depend on the availability of the network connection.

     

    3.1.3 Client console commands in the server

    There are two types of command that the server can accept, so it's important to put your command into the right place. Go back and read the start of section 3 to understand the difference. We'll look at the client console commands here, while dealing with the server console in section 3.2.

    Since we're looking at client console commands we need to work in ClientCommand() in g_cmds.c. Each command that you add is just a straight forward Q_stricmp() comparison that passes control to your handler if passed.

    Note that there is a watershed between commands that can be executed at any point, and those commands which don't make any sense during the intermission (when a level is over and the scoreboard is displayed, but before the next level is loaded).

    If you want TAB completion of the command name in the client, then you'll need to add the name of the command as described at the end of section 3.1.2, above.

     

    I've not found a way for the server to send a client console command to a particular client (such a command could reach either the ui or cgame). However, there is a way that the server can send a command to the client (cgame), and this is covered in section 3.3. Although this seems to be the reciprocal method for sending a client console command from the server, the commands it sends can't be typed into the client - they can only be sent by the server.

     

    3.2 Server console commands

    Notice the change of subject!

    We're now talking about commands that get to the server console, described and defined at the start of section 3.

    These are the commands that can be typed on a client drop down console when the server is running on the same machine, or if the client has administrative privilige on a remote machine. Usually these commands have a drastic effect on gameplay or the behaviour of the server. You want to be careful who you let use these commands, and many of them might be made available through the voting system.

    All of these commands arrive in ConsoleCommand() in g_svcmds.c for processing (note that console in this case means server console). You don't need to do anything special to separate them from the client console commands, this is done for you automatically (and is outside your control!)

    As this snipped version of ConsoleCommand() shows, you need to return qtrue if you do handle the server command. Trace through the code to Svcmd_AddBot_f() in g_bot.c to see how trap_Argv() is used to grab the arguments to addbot.

    qboolean	ConsoleCommand( void ) {
    	char	cmd[MAX_TOKEN_CHARS];
    
    	trap_Argv( 0, cmd, sizeof( cmd ) );
    
    	// code snipped
    
    	if (Q_stricmp (cmd, "addbot") == 0) {
    		Svcmd_AddBot_f();
    		return qtrue;
    	}
    
    	// code snipped
    
    	return qfalse;
    }
    

     

    3.3 Commands issued to the client from the server

    When the server needs to send a command to one or all clients then it uses the following function:

    trap_SendServerCommand(int clientNum, const char* command_string)
    

    The clientNum is the identifying index of the client, look in section 2.3.3 for an example of how to get a clientNum if it isn't provided for you. clientNum may also be -1, in which case the command is broadcast to all connected clients.

    The command will arrive in the client in CG_ServerCommand() in cg_servercmds.c, all ready for the client to deal with.

    In an attempt to make this clear to a possibly confused reader, try the following experiment: start up Q3 and get a map going. Pull down the console and type in "/cp test" (without the quotes of course!). It will show an error message.

    Now look at CG_ServerCommand() in cg_servercmds.c for the cp command. It draws some text in the centre of the screen (cp - center print) and is sent by the server. Can you now see that commands sent by the server aren't connected to the client console?

     

    4. Server scripting - play the game

    The last method of control is aimed purely at the server. You can send a list of commands to the server (to be accurate, the binary executable running the game VM), that are equivalent to running a script. These commands remain in memory and are used (for example) to set up a map rotation. Think of this method as having the same effect as using the /exec command in the drop down console.

    You can add these commands from the user interface part of the source code, or as part of the game code. You are effectively limited to setting up and controlling a server on your machine only, or having a server manipulate its own script. It shouldn't be possible for a local user interface to manipulate a remote server using these functions.

    From within the user interface code:

    void trap_Cmd_ExecuteText( int exec_when, const char *text );
    

    and from within the server code:

    void trap_SendConsoleCommand( int exec_when, const char *text );
    

    Don't be confused by the fact it carries the same name as a function that's used in the client, its behaviour isn't the same. Think of it as acting on the server console only.

    The text argument is easy enough: it's the text string that contains a command or sets a variable. You should add a newline character '\n' when you issue a single command, or you can put a group of commands together in the same string, separated by a semi-colon, and terminated with a newline. If you don't put the newline in then you'll get two commands running together and confusing the executable's script engine.

    The other argument, exec_when, is a bit more difficult to understand from just code reading and without access to the source code for the executable. Here's the allowed values and what I've been able to glean:

    EXEC_NOW
    The string is executed immediately and control won't return to the VM until it's completed. Use with extreme caution in case a command that you use causes the VM to unload and then reload.

    EXEC_INSERT
    This flag causes a command to be "forgotten" about after it's executed. Control will return immediately while the command is queued for execution in the immediate future.

    EXEC_APPEND
    Think of this as appending text to a script file that's stored by the server. Control returns immediately, and values will remain stored for future reference. This is the most commonly used option.

    Although the EXEC_NOW flag is clear enough, it's possible for the other two to be confused. If you start with the idea that EXEC_APPEND is building a script (list of commands) within the executable running the server, then you can see that this script will always be present until the server is shut down. Although you can add new commands and have them executed, you would use the EXEC_INSERT so that it wasn't permanently added to the script being built by EXEC_APPEND.

    It appears that the EXEC_INSERT command isn't used in the user interface, possibly because the server can't be guaranteed to be running. If you do try to use EXEC_INSERT in the user interface, use it with caution.

     

    Example - setting up a server from the ui

    Taken from ServerOptions_Start() in ui_startserver.c, this code fragment shows the bots selected for a map being added to the server script through the use of EXEC_APPEND. This code is run when you create a server from the multiplayer menu, or have a skirmish from the single player menu.

    Notice that each string for the addbot command is finished off with the '\n' character.

    The wait command is interesting: some of the server parameters are set through trap_Cvar_SetValue(), and this allows their value to propagate through to the binary executable. As this code is run when we set up a server on our local machine, the time delay only needs to be a short one.

    int		skill;
    int		n;
    char	buf[64];
    
    // add bots
    trap_Cmd_ExecuteText( EXEC_APPEND, "wait 3\n" );
    for( n = 1; n < PLAYER_SLOTS; n++ ) {
    	if( s_serveroptions.playerType[n].curvalue != 1 ) {
    		continue;
    	}
    	if( s_serveroptions.playerNameBuffers[n][0] == 0 ) {
    		continue;
    	}
    	if( s_serveroptions.playerNameBuffers[n][0] == '-' ) {
    		continue;
    	}
    	if( s_serveroptions.gametype >= GT_TEAM ) {
    		Com_sprintf( buf, sizeof(buf), "addbot %s %i %s\n",
    			s_serveroptions.playerNameBuffers[n], skill,
    			playerTeam_list[s_serveroptions.playerTeam[n].curvalue] );
    	}
    	else {
    		Com_sprintf( buf, sizeof(buf), "addbot %s %i\n",
    			s_serveroptions.playerNameBuffers[n], skill );
    	}
    	trap_Cmd_ExecuteText( EXEC_APPEND, buf );
    }
    

     

    5. The end of the road...

    We've come a long way with this article, and covered a lot of ground that turned out to be more complex that even I'd originally thought. So long as you keep a clear picture in your mind of the differences and relationships between the user interface, the client, the server, and the executable, then you should be able to use this information to the full.

    When I originally planned this article I was going to talk about manipulating information stored in entities too - I'll have to save that for another time as this article is now far too long to do entities justice.

    There are also some bot commands for setting variables, but I've left them out for reasons of space and specialization. The ideas that govern the use of these bot commands are going to be very similar to the conventional methods I've described in this article.

    Let me know if there are any problems with the article, and if it's been of help to you.

    June 16

    使用 IPicture 和 MemDC

      前一段时间,用MFC作个界面程序:画图并在上面写一段文字。原来用IPicture 在DC上 Render ,然后用DC TextOut 出文字,结果文字闪耀。在 OnEraseBkgnd(CDC* pDC) 用了 Mem dc 也没有起作用, 初始还以为刷新快不能做到不闪耀,后来才发现 mem dc 忘了设置 Bitmap ,没有 bitmap 对于内存dc不能画上内容,而 OnPaint 中又没有使用内存DC。导致一直是OnPaint 中DC在画图和写字。
    bool DrawPicture(CDC* pdc, IPicture* pic, const CRect& rcBounds)
    {
     CDC MemDC;  //定义内存DC
     MemDC.CreateCompatibleDC(NULL);   //注意不要写成.CreateCompatibleDC(pdc);
     MemDC.SetBkMode(TRANSPARENT);  //设置透明,使写字没有白色的背景块
     //得到图片的宽度和高度
     SIZE sizeInHiMetric;
     pic->get_Width(&sizeInHiMetric.cx);  //获取加载进pic的图像宽度和高度
     pic->get_Height(&sizeInHiMetric.cy);
     CBitmap bmpFace;   //创建bitmap
     bmpFace.CreateCompatibleBitmap(pdc, rcBounds.Width(), rcBounds.Height()); 
     //将这幅图片选入内存DC,原来memdc不起作用就是因为,bmp图片没有选入到dc中.郁闷
     CBitmap* pOldBmp = MemDC.SelectObject(&bmpFace); 
     //把整个图片 {0,cy,cx,-cy} .拷贝到 memdc 的rcBounds 区域 
     pic->Render(MemDC.GetSafeHdc(), rcBounds.left, rcBounds.top, rcBounds.right, rcBounds.bottom,
      0, sizeInHiMetric.cy, sizeInHiMetric.cx, -sizeInHiMetric.cy, &rcBounds);
     MemDC.SetTextColor(RGB(0,255,0));
     MemDC.TextOut(3,3,m_strFileTips); 
     //拷贝memdc (0,0,rcbounds.width,rcbounds.height) 区域到 pdc
     pdc->BitBlt(0,0,rcBounds.Width(),rcBounds.Height(),&MemDC,0,0,SRCCOPY);   //拷贝 memdc - > dc
     MemDC.SelectObject(pOldBmp);
     bmpFace.DeleteObject();
     MemDC.DeleteDC();
     return true;
    }
     
    对于镂空图可以使用
    void DrawTransPic(IPicture* pic,const CRect& rc,COLORREF clr)
    {
     if(pic == NULL)
      return;
     CDC dc;  //内存DC
     dc.CreateCompatibleDC(NULL);
     CBitmap bmp;
     bmp.CreateCompatibleBitmap(&m_MemDC,m_RtDC.Width(),m_RtDC.Height());  //m_RtDC为内存DC区域
     CBitmap* pold = dc.SelectObject(&bmp);
     SIZE sizeInHiMetric;
     pic->get_Width(&sizeInHiMetric.cx);
     pic->get_Height(&sizeInHiMetric.cy); 
     //拷贝整个图片到dc整个客户区
     pic->Render(dc.GetSafeHdc(), 0, 0, m_RtDC.Width(), m_RtDC.Height(),
        0, sizeInHiMetric.cy, sizeInHiMetric.cx, -sizeInHiMetric.cy, NULL);   //把 pic 画到真个dc 上
     //这里的m_MemDC 也是内存DC,拷贝dc 整个区域到m_MemDC 的 rc 区域,clr 为要剔除掉的关键色
     m_MemDC.TransparentBlt(rc.left,rc.top,rc.Width(),rc.Height(),&dc,0,0,m_RtDC.Width(),m_RtDC.Height(),clr);
     bmp.DeleteObject();
     dc.SelectObject(pold);
     dc.DeleteDC();
    }
    最后 m_RealDC 真正的设备 dc, 获取全部 memdc 内容
    m_RealDC->BitBlt(0,0,m_RtDC.Width(),m_RtDC.Height(),&m_MemDC,0,0,SRCCOPY);
    可以在 OnPaint() 和 OnEraseBkgnd(CDC* pDC) 调用该函数DrawPicture 函数,OnEraseBkgnd(CDC* pDC) 注意要 return TRUE;
    使用后不再有闪耀的问题。

    搜索文件目录获取文件列表

    //dest 写入的目标,size 目标大小,写入的格式,写入的内容
    void Com_sprintf( char *dest, int size, const char *fmt, ...)
    {
     int  len;
     va_list  argptr;
     char bigbuffer[32000]; // big, but small enough to fit in PPC stack
     va_start (argptr,fmt);
     len = vsprintf (bigbuffer,fmt,argptr);
     va_end (argptr);
     if ( len >= sizeof( bigbuffer ) )
     {
      ASSERT("Com_sprintf: overflowed bigbuffer" );
     }
     if (len >= size)
     {
      ASSERT ("Com_sprintf: overflow");
     }
     strncpy( dest, bigbuffer, size-1 );
     dest[size-1] = 0;
    }
     
    //字符串比较,忽略大小写
    int Q_stricmpn (const char *s1, const char *s2, int n)          
    {
     int  c1, c2;
     do
     {
      c1 = *s1++;
      c2 = *s2++;
      if (!n--)
      {
       return 0;  // strings are equal until end point
      }
      if (c1 != c2)
      {
       if (c1 >= 'a' && c1 <= 'z')
       {
        c1 -= ('a' - 'A');
       }
       if (c2 >= 'a' && c2 <= 'z')
       {
        c2 -= ('a' - 'A');
       }
       if (c1 != c2)
       {
        return c1 < c2 ? -1 : 1;
       }
      }
     } while (c1);
     return 0;  // strings are equal
    }
    int Q_stricmp (const char *s1, const char *s2)
    {
     return Q_stricmpn (s1, s2, 99999);
    }
     
    //basedir 根目录,subdirs 子目录,存放文件的列表,列表大小
    //最开始选的目录为根目录,开始 subdirs 可设为 NULL
    int Sys_ListFilteredFiles( const char *basedir, char *subdirs, char *filter, char **list, int *numfiles )
    {
     char  search[MAX_OSPATH], newsubdirs[MAX_OSPATH];
     char  filename[MAX_OSPATH];
     intptr_t findhandle;
     struct _finddata_t findinfo;
     if ( *numfiles >= MAX_FOUND_FILES - 1 )
     {
      return -1;
     }
     if (strlen(subdirs))                                     
     {
      Com_sprintf( search, sizeof(search), "%s\\%s\\*", basedir, subdirs );
      Com_sprintf( filename, sizeof(filename), "%s\\%s", basedir, subdirs );
     }
     else
     {
      Com_sprintf( search, sizeof(search), "%s\\*", basedir );                 
      //搜索的目录,* 表示搜多所有文件。*.mp3 即搜索mp3文件
      Com_sprintf( filename, sizeof(filename), "%s", basedir );
     }
     findhandle = _findfirst (search, &findinfo);
     if (findhandle == -1)                          
     {
      return 0;        //如果是*.mp3 之类,没有任何文件
     }
     do
     {
      if (findinfo.attrib & _A_SUBDIR) //是否为目录
      {
       if (Q_stricmp(findinfo.name, ".") && Q_stricmp(findinfo.name, ".."))  //不是. 或者..
       {
        if (strlen(subdirs))
        {
         Com_sprintf( newsubdirs, sizeof(newsubdirs), "%s\\%s", subdirs, findinfo.name);
        }
        else
        {
         Com_sprintf( newsubdirs, sizeof(newsubdirs), "%s", findinfo.name);
        }
        Sys_ListFilteredFiles( basedir, newsubdirs, filter, list, numfiles );
       }  
      }
      else
      {
       if ( *numfiles >= MAX_FOUND_FILES - 1 )
       {
        return -1;
       }    
       list[*numfiles] = new char[MAX_PATH];
       Com_sprintf( list[*numfiles], MAX_PATH, "%s\\%s", filename,findinfo.name);      //存入文件名到列表中
       (*numfiles)++;
      }
     } while ( _findnext (findhandle, &findinfo) != -1 );
     _findclose (findhandle);
     return 0;
    }
     
     #define MAX_FOUND_FILES 0x1000       //处理不超过4096个文件。
     char* list[MAX_FOUND_FILES];
     int count = 0;
     if(Sys_ListFilteredFiles(str,"",NULL,list,&count))
     {
      MessageBox("文件过多,请重新选择更细的目录");
      for(int i=0;i<count;i++)
      { 
       delete list[i];
      }
     }

    使用对话框打开文件夹获得路径

    一般用CFileDialog 只能打开文件而不能打开文件夹。实现这个功能可以使用windows api  
    LPITEMIDLIST SHBrowseForFolder(LPBROWSEINFO lpbi);
     
     BROWSEINFO bi;
     char path[MAX_PATH];
     LPITEMIDLIST pbi;
     ZeroMemory(&bi,sizeof(BROWSEINFO));          
     bi.pszDisplayName = path;                           //用来存放文件夹名称的缓冲区
     pbi = SHBrowseForFolder(&bi);                      //打开文件夹对话框
     SHGetPathFromIDList(pbi,path);                   //path 就是获得的文件夹路径
     
    June 11

    修改 shotgun

    shotgun 是 quake 里面的霰弹枪,射出去的子弹随机分布在一定范围内。目标:修改霰弹枪,当人物蹲射时可以增加霰弹枪精度,使子弹不至过于分散。注:霰弹一次出去为11发。而且不像火箭那样,没有飞行时间。所以当开枪后就计算是否命中。
    PMF_DUCKED 标志表示人物处于下蹲状态。
    #define PMF_DUCKED   1
    #define PMF_JUMP_HELD  2
    #define PMF_BACKWARDS_JUMP 8  // go into backwards land
    #define PMF_BACKWARDS_RUN 16  // coast down to backwards run
    #define PMF_TIME_LAND  32  // pm_time is time before rejump
    #define PMF_TIME_KNOCKBACK 64  // pm_time is an air-accelerate only time
    #define PMF_TIME_WATERJUMP 256  // pm_time is waterjump
    #define PMF_RESPAWNED  512  // clear after attack and jump buttons come up
    #define PMF_USE_ITEM_HELD 1024
    #define PMF_GRAPPLE_PULL 2048 // pull towards grapple location
    #define PMF_FOLLOW   4096 // spectate following another player
    #define PMF_SCOREBOARD  8192 // spectate as a scoreboard
    #define PMF_INVULEXPAND  16384 // invulnerability sphere set to full size
    #define PMF_ALL_TIMES (PMF_TIME_WATERJUMP|PMF_TIME_LAND|PMF_TIME_KNOCKBACK)
     
    在ShotgunPattern 函数中加入一个变量 accuracyFactor  (在 hitClient 声明后面):
     qboolean hitClient = qfalse;
     int   accuracyFactor = 4;
     // derive the right and up vectors from the forward vector, because
     // the client won't have any other information
     VectorNormalize2( origin2, forward );
     PerpendicularVector( right, forward );
     CrossProduct( forward, right, up );
     oldScore = ent->client->ps.persistant[PERS_SCORE];
     if(ent->client->ps.pm_flags & PMF_DUCKED)  //是否蹲下
     {
      accuracyFactor = 1;
     }
     // generate the "random" spread pattern
     for ( i = 0 ; i < DEFAULT_SHOTGUN_COUNT ; i++ ) {
      r = Q_crandom( &seed ) * DEFAULT_SHOTGUN_SPREAD * accuracyFactor * 16;   //这里调节
      u = Q_crandom( &seed ) * DEFAULT_SHOTGUN_SPREAD * accuracyFactor * 16;
      VectorMA( origin, 8192 * 16, forward, end);
      VectorMA (end, r, right, end);
      VectorMA (end, u, up, end);
      if( ShotgunPellet( origin, end, ent ) && !hitClient ) {
       hitClient = qtrue;
       ent->client->accuracy_hits++;
      }
     }
     
    当蹲下是和原来一样,而站立的时候,霰弹枪的子弹将会更加发散。
    同时注意在 ShotgunPattern 函数前面的注释,// this should match CG_ShotgunPattern
    和 cg_weapons.c 中的 CG_ShotgunPattern 比较起来你会发现两个函数极为相似,除了 CG_ShotgunPattern 被声明为 static ,就是说此函数只能在,cg_weapons.c 文件中使用。
    ================
    CG_ShotgunPattern
    Perform the same traces the server did to locate the
    hit splashes
    ================
    */
    static void CG_ShotgunPattern( vec3_t origin, vec3_t origin2, int seed, int otherEntNum ) {
     int   i;
     float  r, u;
     vec3_t  end;
     vec3_t  forward, right, up;
     int   accuracyFactor = 4; //add mzz,调整霰弹枪精度
     // derive the right and up vectors from the forward vector, because
     // the client won't have any other information
     VectorNormalize2( origin2, forward );
     PerpendicularVector( right, forward );
     CrossProduct( forward, right, up );
     if((otherEntNum == cg.snap->ps.clientNum) && (cg.snap->ps.pm_flags & PMF_DUCKED))  //加入的蹲下的判断
     {
      accuracyFactor = 1;
     }
     // generate the "random" spread pattern
     for ( i = 0 ; i < DEFAULT_SHOTGUN_COUNT ; i++ ) {
      r = Q_crandom( &seed ) * DEFAULT_SHOTGUN_SPREAD * accuracyFactor * 16;   //调整散射范围
      u = Q_crandom( &seed ) * DEFAULT_SHOTGUN_SPREAD * accuracyFactor * 16;
      VectorMA( origin, 8192 * 16, forward, end);
      VectorMA (end, r, right, end);
      VectorMA (end, u, up, end);
      CG_ShotgunPellet( origin, end, otherEntNum );
     }
    }
    Q_crandom( &seed ) 取圆内随机数。
    两个函数之所以这样主要为了解决网络带宽问题。这个在后面在做详细的探讨
     
    同步客户端:
    尽管在CG_ShotgunPattern 函数中不能使用 gentity_t。但在客户端当你有 一个整形变量如otherEntNum,在客户端有个变量cg, cg 是一个十分重要的变量,因为他带给你最重要的人物: 玩家
    cg.snap->ps.clientNum 代表当前客户端的 number, 对于客户端主要关心一个实体:屏幕前的玩家。cg 拥有服务端一部分属性,如玩家是否蹲下。cg.snap->ps.pm_flags
     
    对于移动的惩罚: 当前移动状态存在于 ps.velocity[] 中,这是一个 vec3_t 数据类型。velocity[0] 代表前后移动,velocity[1] 代表左右移动,velocity[2] 代表上下移动。
     
    在 服务端 shotgunpattern 中加入判断
    else if (ent->client->ps.velocity[0] || ent->client->ps.velocity[1])
      accuracyFactor = 3;
    else if (ent->client->ps.velocity[2])
      accuracyFactor = 4;
     
    在客户端为:
      if(otherEntNum == cg.snap->ps.clientNum)
      {
        if(cg.snap->ps.pm_flags & PMF_DUCKED)
           accuracyFactor = 1;
        else if (cg.snap->ps.velocity[0] || cg.snap->ps.velocity[1])
           accuracyFactor = 3
        else if(cg.snap->ps.velocity[2])
           accuracyFactor = 4;
      }
     
    一些关于榴弹的参数:
     在 fire_grenade 中 blot->s.eFlags = EF_BOUNCE_HALF; 实体的一种标志,可以让榴弹在关卡表面上弹跳而不是爆炸
     blot->s.pos.trType 为 TR_GRAVITY 表示弹道受道重力影响,火箭为TR_LINEAR 即走直线
     s.eType 是 ET_MISSILE 当遇到可以破坏的目标时, 函数 G_MissileImpact 调用。
     游戏当前时间 (level.time)
    June 05

    让火箭自动导航

    注意:前提知道如何调用自己编译的dll,而不是使用quake的qvm
    // client data that stays across multiple respawns, but is cleared
    // on each level change or team change at ClientBegin()
    // 客户端 clientPersistant_t数据会随关卡或者团队改变而改变。
    typedef struct {
     clientConnected_t connected; 
     usercmd_t cmd;    // we would lose angles if not persistant
     qboolean localClient;  // true if "ip" info key is "localhost"
     qboolean initialSpawn;  // the first spawn should be at a cool location
     qboolean predictItemPickup; // based on cg_predictItems userinfo
     qboolean pmoveFixed;   //
     char  netname[MAX_NETNAME];
     int   maxHealth;   // for handicapping
     int   enterTime;   // level.time the client entered the game
     playerTeamState_t teamState; // status in teamplay games
     int   voteCount;   // to prevent people from constantly calling votes
     int   teamVoteCount;  // to prevent people from constantly calling votes
     qboolean teamInfo;   // send team overlay updates?
    } clientPersistant_t;
    在第14行加入 qboolean homing_status 。然后变成下面这样
     .....
     qboolean homing_status;  //用于切换自动导航和正常状态
     qboolean teamInfo;   // send team overlay updates?
    } clientPersistant_t;
     
    然后创建一个控制台命令来设置这个变量。打开 g_cmds.c 文件1598行,你将会看到
    void ClientCommand( int clientNum ) {
     gentity_t *ent;
     char cmd[MAX_TOKEN_CHARS];
     
    在函数的最后你将看到:
     else if (Q_stricmp (cmd, "stats") == 0)
      Cmd_Stats_f( ent );
     else
      trap_SendServerCommand( clientNum, va("print \"unknown cmd %s\n\"", cmd ) );
    }
    在  Cmd_Stats_f( ent ); 后面我们在加入1行
    else if(Q_stricmp(cmd,"homing") == 0)
      Cmd_Homing_f( ent );
     else
      trap_SendServerCommand( clientNum, va("print \"unknown cmd %s\n\"", cmd ) );
    }
    之后加入Cmd_Homing_f(gentity_t* ent) 处理函数
    void Cmd_Homing_f(gentity_t* ent){
     if(ent->client->pers.homing_status == 1){
      trap_SendServerCommand(ent-g_entities,va("print \Homing Missile are off.\n\""));
      ent->client->pers.homing_status = 0;
     }
     else{
      trap_SendServerCommand(ent-g_entities,va("print \Homing Missile are on.\n\""));
      ent->client->pers.homing_status = 1;
     }
    }
     
    下面开始创建火箭的自动导航:
    火箭查找一点范围内的目标。如果找到目标,将修改它的方向,朝向目标。在 g_utils.c 中加入如下代码用于寻找目标
    gentity_t* findradius(gentity_t* from,vec3_t org,float rad){
     vec3_t eorg;
     int j;
     if(!from)
      from = g_entities;
     else
      from++;
     for(;from <&g_entities[level.num_entities];from++){  //level.num_entities关卡中实体数目,用MAX_CLIENTS 更好一下。
      if(!from->inuse)
       continue;
      for(j=0;j<3;j++)
       eorg[j] = org[j]- (from->r.currentOrigin[j]);//+(from->r.mins[j]+from->r.maxs[j])*0.5);
      if(VectorLength(eorg) > rad)  //检测实体之间的距离
       continue;
      return from;
     }
     return NULL;
    }
    接着加入函数:
    /*
      检查两个实体之间是否可见
    */
    qboolean visible(gentity_t* ent1,gentity_t* ent2){
     trace_t trace;
     //ent1 到 ent2 之间发射一条线检测是否与其他物体碰撞
     trap_Trace(&trace,ent1->s.pos.trBase,NULL,NULL,ent2->s.pos.trBase,ent1->s.number,MASK_SHOT);
     if(trace.contents & CONTENTS_SOLID){ //轨迹中与固体发生碰撞
      return qfalse;
     }
     return qtrue;
    }
    这个函数从1个实体像另一个发射一条线,看是否可见。如果轨迹经过了墙。返回false,否则返回 true
    接着你需要在 g_local.h 中放入 visible 和 findradius 两个函数的声明
    gentity_t* findradius(gentity_t* from,vec3_t org,float rad); 
    qboolean visible(gentity_t* ent1,gentity_t* ent2);   
    当这些完成之后打开 g_missile.c 文件 加入一个新的 think 函数
    void G_HomingMissile( gentity_t *ent ) {
     gentity_t* target = NULL;
     gentity_t* rad = NULL;
     vec3_t dir,dir2,raddir,start;
     while((rad = findradius(rad,ent->r.currentOrigin,600.0f)) != NULL){  //如果在600个单位范围是否有物体
      if(!rad->client)
       continue;        //如果目标不是一个玩家,继续
      if(rad == ent->parent)
       continue;      //是否是开枪的人,不要把自己炸了
      if(rad->health <=0 )
       continue;     //目标是否死亡,不去炸尸体
      if(rad->client->sess.sessionTeam == TEAM_SPECTATOR)  //是否为观众
       continue;
      if((g_gametype.integer == GT_TEAM || g_gametype.integer == GT_CTF)&&
       rad->client->sess.sessionTeam == rad->parent->client->sess.sessionTeam)  //是否为同一队伍玩家
       continue;
      if(!visible(ent,rad))   //目标是否可见
       continue;
      VectorSubtract(rad->r.currentOrigin,ent->r.currentOrigin,raddir);
      raddir[2] += 16;
      if(target == NULL || (VectorLength(raddir)<VectorLength(dir))){ //感觉第二个条件判断不该存在。
       target = rad;
       VectorCopy(raddir,dir);
      }
     }
     if(target != NULL)
     {
      VectorCopy(ent->r.currentOrigin,start);
      VectorCopy(ent->r.currentAngles,dir2);
      VectorNormalize(dir);         //归一化dir矢量
      //VectorScale(dir,0.2,dir);  //注掉更好一点,不至于来不及计算飞过头。即使飞过去也来得及回来
      //VectorAdd(dir,dir2,dir);   //原来作用是逐步转向,即使要这个,也不需要上面的 0.2 系数。
      //VectorNormalize(dir);
      VectorCopy(start,ent->s.pos.trBase);       //ent.s.pos 负责移动物体
      VectorScale(dir,600,ent->s.pos.trDelta);   //在指向目标的矢量上移动600个单位 ent->s.pos.trDelta = dir * 600
      //如果用 VectorCopy(raddir,ent->s.pos.trDelta) 将会发现有趣的现象,火箭一直跟着人跑,人不停,就追不上,呵呵
      SnapVector(ent->s.pos.trDelta);             //修改矢量浮点数成员x,y,z 为 int
      //VectorCopy(start,ent->r.currentOrigin);   //
      VectorCopy(dir,ent->r.currentAngles);     //改变方向
     }
     ent->nextthink = level.time + 100;
    }
     
    之后进入关卡可以在控制台输入\Homing 来调整是否使火箭自动导航
    June 04

    实体 - ENTITIES

     
    ARTICLE 1 - ENTITIES
    by SumFuka

    This is the first in a series of 'articles' explaining various things about the quake3 game world. Every wondered how you interact with the game world ? This is the first question we're going to tackle... by working from the ground up. Quake's design hasn't changed much from quake1 thru to quake3... the most basic thing in the world is - yes, an entity !

    AN ENTITY ?

    Virtually anything you can name in the game world is an entity. A rocket ? That's a simple example of an entity. An ammo pack ? An entity. A player ? Again, that's an entity (albeit a special one). Let's take an example... urm... ummm... let's fire a rocket ! Here's how it works.
     
    事实上任何游戏世界中你可以叫出名字的都是实体。火箭筒? 就是一个简单的实体的例子。一个弹药夹?是实体,一个玩家也是实体。让我们举个例子。。。如用火箭筒开火。下面是其工作过程。
    1. You press the fire button    你开枪射击
    2. An empty entity slot is chosen (i.e. a slot that is not inuse)  一个空的实体槽被选择。
    3. A rocket entity is created in that slot    在槽中创建一个火箭筒实体
    4. The rocket settings are defined (positioned just in front of you, aimed in a certain direction, moving at 900 units/second, etc)    设置火箭筒(位于你前面,朝向一个方向,900单位/秒移动速度等等)
    5. The rocket moves through the world until an event is triggered : 
      • If the rocket hits something damageable (e.g. a player or a wall) then it explodes and the entity is removed
      • If the rocket doesn't hit anything within 10 seconds, it explodes and the entity is removed
      • If the rocket hits the sky then the entity is removed (no explosion).
      • 如果火箭筒击中可破坏的东西(如玩家或者墙壁),他会爆炸,实体被移除
      • 如果火箭筒在10秒内没有击中任何东西。他将爆炸移除实体
      • 如果火箭筒击中天空实体将被移除(不会爆炸)
    6. Once the entity is removed, the slot is marked not inuse and may be re-used

        一旦一个实体被移除,槽将被标为未使用。将等待再次被使用

    As you can see, a rocket's life is relatively short ! You might be thinking, is there a limit to the number of rockets that can simultaneously be flying around the map ? Well, yes.
    在地图上同时出现的火箭筒是否有限制? 是的

    HOW MANY ENTITIES EXIST IN THE QUAKE WORLD ?
    在 quake 世界中能存在多少实体?

    Time to do some sleuth work. By the way, when we refer to the quake 'world', we mean the game world and everything in it (not 'quakeworld'). What's in the quake world ? Lots of things... like a map, like the players, and like... ENTITIES ! Jump into MSVC and do a "Find in Files" on 'g_entities'. In g_main.c at line 17 we can find :

    quake 世界存在很多东西,如地图,玩家,还有实体。在msvc 中查找'g_entities'.在 g_main.c 中你会发现:
     gentity_t		g_entities[MAX_GENTITIES];
    This line says that there exists a finite array of entities in the game world. This array is called 'g_entities' and is MAX_GENTITIES long. So what is the constant MAX_GENTITIES ?? Do a "Find in Files" on MAX_GENTITIES and we find q_shared.h line 718 (and 717) to be quite interesting :
     #define	GENTITYNUM_BITS		10		// don't need to send any more
     #define	MAX_GENTITIES		(1 << GENTITYNUM_BITS)
    
    Ok, Carmack is exhibiting some guru-level C syntax here. Take my word that (x << y) means to double x y times. Given that ENTITYNUM_BITS is 10, MAX_GENTITIES is therefore 2 to the power of 10, or 1024. In other words, there is room in the world for approximately 1024 rockets, players, weapons, armors etc at any one time.
    这个有限的数组定义了游戏世界的实体数量。2的10次方。或者1024。就是同时的火箭筒,玩家,武器等等不能超过1024个

    CAN THE ENTITIES RUN OUT ?

    What would happen if you sat at one end of a very long space map with the RL and held down the fire button ? Assuming that your rockets fly for the full ten seconds, you can have about 10 rockets in the air at once. (Remember that when a rocket explodes the entity can be re-used by the next rocket virtually right away). If you start a 32 player game with your mates and you all sit and fire into space, there would be about 320 rockets flying through the air at any one time. We still haven't run out of entities...

    There is a certain number of entities that are permanently used during the whole game (for example, players, weapons and items). All other non-permanent entities follow the cycle create - live a short life - reuse. This is something to think about when you're coding mods - If you make a cluster grenade that splits into 100 mini grenades, then one idiot could quickly run your world out of entities by firing a bunch of cluster grenades in quick succession.
    除了一定数量的实体在整个游戏是永远存在的(例如,玩家,武器物品等等)。其他临时的实体都是在创建-短暂生存-重用之间循环。这是你在编写mod代码时需要注意的。如果你制作一种手榴弹,能爆炸能100个更小的手榴弹。当你开火时,将很快超过游戏世界的实体数量

    What happens if your world runs out of entities ? Assume the worst-case scenario, that your server will crash. If an entity is going to exist for a relatively long time, make sure that it isn't possible for huge numbers of them to exist at the same time.
    当实体数量溢出将会发生什么?你的服务程序将会crash.如果一个实体存在时间较长。确定在同一时刻不会太多存在

    WHAT'S IN AN ENTITY ?

    Let's look at g_local.h, starting at line 49 :
    struct gentity_s {
    	entityState_t	s;	// communicated by server to clients
    	entityShared_t	r;	// shared by both the server system and game
    
    	// DO NOT MODIFY ANYTHING ABOVE THIS, THE SERVER
    	// EXPECTS THE FIELDS IN THAT ORDER!
    	//================================
    
    	struct gclient_s	*client;			// NULL if not a client
    
    	qboolean	inuse;
    
    	char		*classname;		// set in QuakeEd
    	int		spawnflags;		// set in QuakeEd
    
    	qboolean	neverFree;		// if true, FreeEntity will only unlink
    						// bodyque uses this
    
    	int		flags;			// FL_* variables
    
    	char		*model;
    	char		*model2;
    	int		freetime;		// level.time when the object was freed
    	
    	int		eventTime;	// events will be cleared
    	// EVENT_VALID_MSEC after set
    	qboolean	freeAfterEvent;
    	qboolean	unlinkAfterEvent;
    
    	qboolean	physicsObject;	// if true, it can be pushed by movers and fall
    					// off edges all game items are physicsObjects, 
    	float		physicsBounce;	// 1.0 = continuous bounce, 0.0 = no bounce
    	int		clipmask;	// brushes with this content value will be collided
    					// against when moving.  items and corpses
    					// do not collide against players, for instance
    
    	// movers
    	moverState_t moverState;
    	int			soundPos1;
    	int			sound1to2;
    	int			sound2to1;
    	int			soundPos2;
    	int			soundLoop;
    	gentity_t	*parent;
    	gentity_t	*nextTrain;
    	gentity_t	*prevTrain;
    	vec3_t		pos1, pos2;
    
    	char		*message;
    
    	int		timestamp;	// body queue sinking, etc
    
    	float		angle;		// set in editor, -1 = up, -2 = down
    	char		*target;
    	char		*targetname;
    	char		*team;
    	gentity_t	*target_ent;
    
    	float		speed;
    	vec3_t		movedir;
    
    	int		nextthink;
    	void		(*think)(gentity_t *self);
    	void		(*reached)(gentity_t *self);	// movers call this when
    						// hitting endpoint
    	void		(*blocked)(gentity_t *self, gentity_t *other);
    	void		(*touch)(gentity_t *self, gentity_t *other, trace_t *trace);
    	void		(*use)(gentity_t *self, gentity_t *other, gentity_t *activator);
    	void		(*pain)(gentity_t *self, gentity_t *attacker, int damage);
    	void		(*die)(gentity_t *self, gentity_t *inflictor, gentity_t *attacker,
    			int damage, int mod);
    
    	int		pain_debounce_time;
    	int		fly_sound_debounce_time;	// wind tunnel
    	int		last_move_time;
    
    	int		health;
    
    	qboolean	takedamage;
    
    	int		damage;
    	int		splashDamage;	// quad will increase this w/o increasing radius
    	int		splashRadius;
    	int		methodOfDeath;
    	int		splashMethodOfDeath;
    
    	int		count;
    
    	gentity_t	*chain;
    	gentity_t	*enemy;
    	gentity_t	*activator;
    	gentity_t	*teamchain;	// next entity in team
    	gentity_t	*teammaster;	// master of the team
    
    	int		watertype;
    	int		waterlevel;
    
    	int		noise_index;
    
    	// timing variables
    	float		wait;
    	float		random;
    
    	gitem_t		*item;	// for bonus items
    
    	qboolean	botDelayBegin;
    };
    
    Right up the top there is some important stuff - an entityState_t and entityShared_t... these bits include general stuff
    like the location of the entity, the type of entity it is, the size of the bounding box, etc.

    开始2个比较重要,entityState_t 和 entityShared_t 包含了实体的位置,实体的类型。包围盒的大小等等。
    Next comes struct gclient_s *client; - this is a pointer to additional information, if the entity is a 'client' (i.e. player or bot). If the entity is not a client, then this bit is NULL (unused).
    接着是struct gclient_s* client,如果实体是客户(例如玩家或者机器人),则指向附加信息。如果实体非客户,则为NULL

    Further down we can see heaps of interesting fields - classname, speed, movedir, target, team etc. Not all of these fields are used with all entities... a red armor would not use the 'damage' fields, for example (wheras a rocket would). Most of these are pretty self-explanatory.

    下面可以看到更多信息,类名称,速度,移动方向,目标,队伍等信息,并不是所有实体都用到这些信息。。。如红甲就没有 damage 信息(而火箭就有)。所有这些都是自说明的

    "THINKING" ETC

    Lines 110-116 in g_local.h define function pointers. The names of these are think, reached, blocked, touch, use, pain, die. Although the syntax here is very hardcore (remember, Carmack is a codecutting God), it's quite easy to explain with an example.

    在g_local.h文件110-116中定义了一些函数指针:think,reached,blocked,touch,use,pain,die.这些函数在c里面模拟了虚函数效果

    We want our rockets to explode after 10 seconds. Remember, from g_missile.c :

    	bolt->nextthink = level.time + 10000;
    	bolt->think = G_ExplodeMissile;
    

    This means that after 10 seconds, the rocket 'thinks' and the function G_ExplodeMissile is called on the rocket entity.

    Similarly, a grenade explodes after 2.5 seconds. Can you find the code for this ? (Answer : g_missile.c line 294). 'Thinking' is a nice "fire and forget" mechanism - we create an entity, define what it does at some future time, and then forget about it - the game engine takes care of the entity from then on.

    这意味这经过10秒,火箭开始'思考',在该火箭实体上,函数 G_ExplodeMissile 被调用

    10 times a second, the server checks if each entity is due to 'think'. If yes, the 'think' function is run for that entity. Similarly, other entity functions are called in response to certain events. If a player is killed, then the player_die function is called (see g_client.c line 921). The same goes for touch, blocked, pain, etc.

    1秒10次,服务器检查每个实体是否 think,如果是,该实体think 函数被调用。如果玩家被杀死, player_die 函数被调用 (g_client.c)。同样还有touch,blocked,pain 等等

    PERMANENT ENTITIES & CLIENTS

    In q_shared.h we see that the maximum number of clients (players or bots) - MAX_CLIENTS - is 64. By definition, the first MAX_CLIENTS entities in g_entities are reserved for clients. Arrays in C are numbered from 0, so g_entities[0] is reserved for client 0, g_entities[1] for client 1... up to g_entities[63] for player 63.

    在q_share.h 中我们看到最大的客户端数量 MAX_CLIENTS 64,在g_entities中开始的64个实体为客户端保留。所以g_entities[0] 代表客户端0,g_entities[1] 代表客户端1。直到 g_entities 代表玩家 63

    Just as there exists an array of entities in the world, there also exists an array of 'client information' structures - see line 18 in g_main.c :
    尽管存在世界中存在实体的数组。在 g_main.c 中还有1个客户端信息的数组:

     gclient_t		g_clients[MAX_CLIENTS];
    

    Here we have an array of 64 gclient_t's (client information structures). And each of the first 64 g_entities point to
    a corresponding g_client[x]. For example, g_entities[0]->client points to g_clients[0], etc. We'll have a look at what's in the client information structure another time.

    头64个实体对应64个客户端信息。 g_entities[0]->client 指向 g_clients[0] 等等。
    Well, entities really do make the world go round (well, they actually go around the world, kinda... anyway...). Another time we'll talk about temporary entities (rail trails, blood spurts and similar effects). Till then, remember... "West Side."
    注:rail trails 和  blood 等也是实体(临时实体)