[TUT] Semiclip

For scripting examples for learning how to use AMXX.
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

[TUT] Semiclip

Post by Archive-Poster »

Before we begin, I'd just like to state that this tutorial is intended for intermediate to expert level scripters. You should be able to read all the code I've put here and almost immediately understand what should happen. Also, this tutorial is a bit of a detour from my previous ones; it's more of a discussion than a point by point process. I wouldn't be surprised if some of the code doesn't work (feel free to let me know, I'm always willing to fix it up).

I'm putting this up because there's been many requests for semiclip plugins lately. A lot of people seem to be stuck in my step 1 or 2 (as you'll see as you read further). Hopefully this will help get them on the right track.

For those of you that don't know, semiclip is exactly that: half-way between noclip and clipping. You can pass through other entities, but not through walls, nor can you fly.

I've had to try many different methods of semiclip and I've found that none at all are pretty. I have yet to find one be-all-end-all, but hopefully this will bring someone closer to making one. To start off, I'm going to show you the failed ones:

The Scripter's Initial Instinct - Failed Attempts

I first attacked this using simple stuff. The first (or one of the first) things that should come to your mind on how to solve this is using something like register_touch. Using this forward, we can pick up when 2 entities collide and either let the engine do its work or supersede it. Here's an example of how I tried this:

Code: Select all

#include <amxmodx>
#include <engine>

public plugin_init()
{
    register_plugin("Semiclip Test 1","1.0","Hawk552")
   
    register_touch("player","player","TouchPlayer")
}

public TouchPlayer(/*Ptr,Ptd*/)
    return PLUGIN_HANDLED
This looks pretty nice and easy (of course you'd have to add more like cvar checks and such), but there's one catch: it really doesn't do what you want it to. Because of client prediction, the player gets stopped first and then passes through. If you're using this on the ground it's really not a big deal (although kind of annoying), but when you're using it for something like surfing (like I am), it becomes a big problem.

You can also use fakemeta in a similar way. I'm too lazy to type it out, but you can register FM_Touch, then check whether the touched and toucher are alive (is_user_alive).

The next method I went onto was constantly setting a player to unsolid. Again, this sounds very nice as it eliminates the problem of clipping right before the touch is blocked, but again there is a catch. The script first, of course:

Code: Select all

#include <amxmodx>
#include <engine>

public plugin_init()
    register_plugin("Semiclip Test 2","1.0","Hawk552")

public client_PreThink(id)
    entity_set_int(id,EV_INT_solid,SOLID_NOT)
Very nice and compact. In fact, you can probably not even use client_PreThink and instead do it in a forward like ResetHUD or something similar that isn't called all that often. But again for the hitch: the server crashes when you pass through any trigger_* entities.

Okay, not really a problem. We can run a check to scan around the player and see if they are within any distance of a trigger_* ent and then deactivate semiclip if they are. Sure, but what if 2 players are close to the trigger? They will collide, which is a problem. Then what about maps where the entire ground is trigger_teleports, such as some surf maps? This just gets too complicated. I suppose it has some potential, but I think there are really some better methods.

Potential Solutions

The final method I tried that failed was a direct scan around the player for other players. Essentially, you just scan around the player a certain distance, and if the search turns up another player, semiclip is activated. This method has the most potential, as we'll see in the final script. For now, this is what this would look like:

Code: Select all

#include <amxmodx>
#include <engine>

public plugin_init()
    register_plugin("Semiclip Test 3","1.0","Hawk552")

public client_PreThink(id)
{   
    new Ent,Float:Origin[3],On = SOLID_BBOX
    entity_get_vector(id,EV_VEC_origin,Origin)
    while((Ent = find_ent_in_sphere(Ent,Origin,100.0)) != 0)
        if(is_user_alive(Ent))
            On = SOLID_NOT
   
    entity_set_int(id,EV_INT_solid,On)
}
But we run into yet another problem here: the checking simply isn't fast enough. If we make the radius insanely large, we run into the risk of crashing the server if the user is close to a trigger_* entity. There's no way to solve this the way it is now, client_PreThink is simply too slow.

The Only Known Solution

If you can challenge the above statement, feel free to. I'd like to see a better solution personally, I could use it. But for now, this is the closest to perfect semiclip that I know of. A demonstration of it in use can be found in uSurf. Essentially, it's the last attempt except sped up using an entity thinking every 0.01 seconds. Very very resource intensive, but it gives the most accuracy and responsiveness. Here's the implementation from uSurf with uSurf specific stuff stripped out:

Code: Select all

#include <amxmodx>
#include <fakemeta>

new g_SemiclipThinkerClassname[] = "env_semiclip"
new g_SemiclipThinker

public plugin_init()
{
    register_plugin("Semiclip Test 4","1.0","Hawk552")
    
    register_forward(FM_Think,"ForwardThink")
   
    new Ent = engfunc(EngFunc_CreateNamedEntity,engfunc(EngFunc_AllocString,"info_target"))
    if(!Ent)
        return
   
    set_pev(Ent,pev_classname,g_SemiclipThinkerClassname)
    new Float:Time
    global_get(glb_time,Time)
    set_pev(Ent,pev_nextthink,Time + 0.01)
    dllfunc(DLLFunc_Spawn,Ent)
   
    g_SemiclipThinker = Ent
}

public ForwardThink(Ent)
{
    if(Ent != g_SemiclipThinker)
        return
   
    static Players[32],Playersnum,Player
    get_players(Players,Playersnum)
   
    for(new Count;Count < Playersnum;Count++)
    {           
        Player = Players[Count]
        if(!is_user_alive(Player))
            continue
       
        set_pev(Player,pev_solid,IsColliding(Player) ? SOLID_NOT : SOLID_BBOX)
    }
   
    new Float:Time
    global_get(glb_time,Time)
    set_pev(Ent,pev_nextthink,Time + 0.01)
}

IsColliding(id)
{   
    new Ent,Float:Origin[3]
    pev(id,pev_origin,Origin)
       
    while((Ent = engfunc(EngFunc_FindEntityInSphere,Ent,Origin,36.0)) != 0)
        if(Ent > 0 && Ent <= 32 && is_user_alive(Ent) && Ent != id)
            return true
   
    return false
}
Is this level necessary for most uses? No, most people just want to be able to pass through others. For that, the first method may even work. Feel free to use whatever you want, there's really no standard as they all suck.

As always, if you have any questions, comments or whatever feel free to post. As aforementioned, I invite anyone to challenge anything said here. Hope this helps.
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

I tried to optimize it, tell me what you think:

Code: Select all

#include <amxmodx>
#include <fakemeta>

new g_SemiclipThinkerClassname[] = "env_semiclip"
new g_SemiclipThinker

#define MAX_PLAYERS 32

new g_maxplayers
new bool:g_restart_attempt[MAX_PLAYERS + 1]
new bool:g_is_user_alive[MAX_PLAYERS + 1]

public plugin_init()
{
    register_plugin("Semiclip Test 4","1.0","Hawk552")
    
    register_forward(FM_Think, "forward_Think")
    
    //VEN's method to capture player spawn
    register_event("ResetHUD", "event_hud_reset", "be")
    register_clcmd("fullupdate", "clcmd_fullupdate") 
    register_event("TextMsg", "event_restart_attempt", "a", "2=#Game_will_restart_in")
    register_event("DeathMsg", "event_death", "a")
    
    g_maxplayers = global_get(glb_maxClients)
    
    new Ent = engfunc(EngFunc_CreateNamedEntity,engfunc(EngFunc_AllocString,"info_target"))
    if(!pev_valid(Ent))
        return
    
    set_pev(Ent,pev_classname,g_SemiclipThinkerClassname)
    new Float:Time
    global_get(glb_time,Time)
    set_pev(Ent,pev_nextthink,Time + 0.01)
    dllfunc(DLLFunc_Spawn,Ent)
    
    g_SemiclipThinker = Ent
}

public forward_Think(Ent)
{
    if(Ent != g_SemiclipThinker)
        return
    
    static i,Float:Time
    
    for(i=0;i<=g_maxplayers;i++)
    {
        if(g_is_user_alive[i])
        {
            set_pev(i,pev_solid,IsColliding(i) ? SOLID_NOT : SOLID_BBOX)
        }
    }
    
    global_get(glb_time,Time)
    set_pev(Ent,pev_nextthink,Time + 0.01)
}

stock IsColliding(id)
{
    if(pev(id,pev_flags) & FL_ONGROUND || pev(id,pev_button) & IN_JUMP)
        return false
    
    static Ent,Float:Origin[3]
    
    pev(id,pev_origin,Origin)
    
    while((Ent = engfunc(EngFunc_FindEntityInSphere,Ent,Origin,36.0)) != 0)
        if(Ent > 0 && Ent <= g_maxplayers && g_is_user_alive[Ent])
            return true
    
    return false
}

public clcmd_fullupdate()
{
    return PLUGIN_HANDLED_MAIN
}

public event_restart_attempt()
{
    new players[32], num
    get_players(players, num, "a")
    for (new i; i < num; ++i)
        g_restart_attempt[players[i]] = true
}

public event_hud_reset(id)
{
    if (g_restart_attempt[id])
    {
        g_restart_attempt[id] = false
        return
    }
    event_player_spawn(id)
}

// this function is called on player spawn
public event_player_spawn(id)
{
    g_is_user_alive[id] = true
}

public event_death(id)
{
    g_is_user_alive[read_data(2)] = false
}
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

Optimisation (IsColliding(id)):
new Ent,Float:Origin[3]

Should be:
static Ent,Float:Origin[3]

Reason being well it can be potentially called as often as ForwardThink(Ent).


Optimisation (public ForwardThink(Ent)):
new Float:Time

Should be:
static Float:Time

Same reason as the other.

IMO and I think it is covered on the WIKI, all fast ticking function variables should be declared static.


Cheers
Thanks but that's a really marginal optimization. If it was a huge array, I'd probably add it in, but for a couple of single cell variables I'm not going to bother.
Variable size doesn't matter.

Just notice something your for statement is using new count that is a performance issue! And why did you do this?

Code: Select all

static Players[32],Playersnum,Player
I am referring to this:

Code: Select all

for(new Count;Count < Playersnum;Count++)
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

Basically, if I made it static, it would have to be reset each and every time the function was called. Not only would I have to pull it out of the declaration section of the for loop, I'd also have to zero it. Since the zeroing is where the performance hit is, there's no reason to make it static.
I'm not being funny but it's the creation of variables that cause the performance hit, zeroing increases it but you have to live with that since that is Pawn.

Your for loop is doing this new count, IMO that is bad practice you are showing people! Since it's usually new count = 0 or w/e.

FYI I'm studying application development and I just covered a section regarding this with my tutor. Fast ticking functions should be using either static or global variables if they are being constantly used like you are showing. There is ways and means of not doing this which is down to checking methods within the function. External functions which I mentioned previously is trival and more than likely not worth it.
I don't think you understand... "new Count = 0" is useless as it's automatically declared as 0. If I did make it static, I would have to zero it again every time the function was called. All of the performance hit is in zeroing it (Someone said that 3 or 4 times now).

I don't see your point. I think it's not only easier to use 'new' but also makes more sense as it's more practical.
What you are essentially doing:

Script:
new variable

Pawn:
int variable; // created whenever a new variable is created
variable = 0; // due to new being used Pawn 0's it


Using static...
Script:
static variable

Pawn:
static variable; // created only once


Do you understand now? Yes you have to manually 0 the item but that is what for statements are for

Anyway enough said, no more response from me now!
All of the performance hit is in zeroing it (Someone said that 3 or 4 times now).
LOL at that! Means whenever you change a variable it hits performance issues regardless if you 0 it or change it to 1
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

the first method with making them not solid a soon as they touch, because it is called after they touch, which means that they have already collided and get a rebound off, slowing you down, which kind of defeats the purpose. I have tried making this for a bunnyhop seerver so they could jump through teamates, but they had already collided screwing up their jump by time I made them not solid. I think we solved that problem, I dont remember, try and find that topic.
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

PreThink is called once per frame, per client.

Touch is calculated at most once per frame.

The way you have it (with the constantly thinking entity) is equivalent to having a server frame hook.

The prethink hook just may not have been working because physics may be done before prethink is called, not sure on that.
If the server is getting much higher FPS than the client then that's why that method works better. On a slower server nothing will work properly.

The prethink hook is just not fast enough because a lot of clients get really low FPS. It's really a tradeoff, but the server tends to be faster (even though the think isn't really being called every 0.01 seconds).
Prethink doesn't depend on client FPS, it depends on server FPS.
I did not know it was based on the server's FPS, though I do avoid using it for velocity calculations because some servers are slower than others and that can really screw things up.
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

Instead of using FindEntInSphere for IsColliding(id)

You could also do this.

Code: Select all

bool:IsColliding(player, Float:distance)
{
    new Float:myOrigin[3];
    new Float:orig[3];
    new Float:dir[3];
    new id;
    new Float:maxdistance = distance * distance;

    pev( player, pev_origin, myOrigin );

    for(id=1; id<=get_maxplayers(); id++)
    {
          if(id == player || !is_user_connected(id) || !is_user_alive(id))
                continue;
    
          pev( id, pev_origin, orig);
          
          orig[0] = orig[0] - myOrigin[0];
          orig[1] = orig[1] - myOrigin[1];
          orig[2] = orig[2] - myOrigin[2];
          distance = (orig[0]*orig[0]) + (orig[1]*orig[1]) + (orig[2]*orig[2]);

           if(distance <= maxdistance)
                 return true;
    }
    return false;
}
FindEntInSphere does this distance check against every single entity in the map. This just checks every player. Now you can go crazy using semi-clip and not worry about it slowing your server down.
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

has anything better than the methods in the first post been made yet?

I think I am gonna start trying my own methods if there haven't, but I don't wanna be reinventing the wheel here.

My big idea is to put some sort of invisible, noclipped entities ( boxes ) on every spawned player, then when the boxes of two players touch set those players to not solid. Also I wanna investigate what amxx things could help tweak the client prediction...I haven't really been able to find anything regarding this in the funcwiki except maybe setting a player's movetype to FL_ONTRAIN....lol
shitty ? You can make this without any "lol" ents...when 2 players touched set to unsolid, but that method is useless and inefficient!And what predicition? -_-

Use OneEyed version...should work!
OneEyed posted a more efficient method for hawk's third semi clip method which is still inefficient due to the entity that thinks 100 times a second and not to mention that it still runs the risk of crashing the server because trigger_* entites and not solid players do not mix well...and I dunno why I just paraphrased hawk....

this is the best and most efficient way to semiclip:

Code: Select all

#include <amxmodx>
#include <amxmisc>
#include <fakemeta>

#define PLUGIN    "Hobo Semi Clip Lite"
#define AUTHOR    "hoboman313"
#define VERSION    "1.0"

#define MAX_PLAYERS 32
#define SEMI_CLIP_DISTANCE 100.0

new sclippedPlayer[MAX_PLAYERS+1], maxplayers

public plugin_init()
{
    register_plugin(PLUGIN, VERSION, AUTHOR)
        
    maxplayers = get_maxplayers()
    
    register_forward( FM_ShouldCollide, "make_unsolid" )
    register_forward( FM_PlayerPreThink, "make_solid" )
}

public make_unsolid( entTouched, entOther )
{    

    if( is_user_alive(entOther) && is_user_alive(entTouched) && close_enough(entTouched, entOther )  )
    {
        if( !sclippedPlayer[entOther] )
        {
            set_pev( entOther, pev_solid, SOLID_NOT )
            
            sclippedPlayer[entOther]=true
        }
        
        if( !sclippedPlayer[entTouched] )
        {
            set_pev( entTouched, pev_solid, SOLID_NOT )
            
            sclippedPlayer[entTouched]=true
        }
    }
}


public make_solid( id )
{
    if( sclippedPlayer[id] && is_user_alive(id) )
    {        
        for( new i=1; i<=maxplayers; i++ )
        {
            if( is_user_alive(i) && i!=id && close_enough(i, id) )
            {
                return
            }
        }
        
        sclippedPlayer[id]=false
        set_pev( id, pev_solid, SOLID_BBOX )
    }
}


public bool:close_enough(ent1, ent2)
{
    new Float:origin[3], Float:origin2[3]
    
    pev(ent1, pev_origin, origin )
    pev(ent2, pev_origin, origin2 )
    
    if( get_distance_f( origin, origin2 ) <= SEMI_CLIP_DISTANCE )
        return true

    return false    
}
I was gonna release it as a new plugin, but now I have given up ( and quite honestly I am sick of this plugin, lol ) because if two players are sitting inside each other they are SOLID_NOT and cannot be shot
That's neither the best, nor the most efficient way. It's actually one of the least efficient. Try using it on a surf server.
the only reason I used tasks was because I wanted to single out the time when the player is in SOLID_NOT state. As you can see I replaced that part using prethink now.


I still stand by the statement that my method is still a million times more efficient and is the best semiclipping method available. If you really thought of it already then I don't understand why you didn't bother posting it ( could have saved me some time ).
Of course, I cannot argue that my method may not work as well on the surf servers, but that is a kind of a far out case with extreme conditions ( extreme speeds ). But it is accurate enough for pretty much all the other cases.


oh and Hawk, do you even realize that you are contradicting yourself from your first post:
thinking every 0.01 seconds. Very very resource intensive, but...
and now
Also, my method isn't as inefficient as you may think
If you really want to argue that looping through all the players and checking all the entities around them every 10 milliseconds is more efficient than using ShouldCollide ( which is only called when the players are getting relatively close together ), I would really love to hear that argument. No, really...
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

Your method isn't the best method available. It's actually extremely resource consuming. The forward client_PreThink gets called every physics frame before rendering and ShouldCollide is called every frame when a collision should happen, which means that it's actually double the amount of calls each frame that you're trying to activate semiclipping. Do you know what's even worse about it? Your method equates to 2*players calls every frame. Ouch!

Why not compress it down to one forward and just use a native (get_players) to avoid going back and forth through the VM layer up to 64 times? My method is efficient that way. Since you apparently don't seem to understand the difference between efficiency and resource consumption, I don't think it would be fair me to keep going on to why your method actually sucks. I will anyway, though. I asked you a few posts ago to try your method on a surf server. You said something about "[...] [having] no idea wut [sic] the hell [you were] talking about [...]" (which, may I add, really gives me confidence in your opinion) and that it "[...] may not work as well on the surf servers [...]", so I can only assume that you're rejecting the notion that surf servers are basically the only application of semiclip. So basically, I really don't understand where you're going with this.

I'm also kind of surprised that you actually think that the ent thinks every 0.01 seconds. It really thinks at a rate dependent on both the server frames rendered and the time elapsed since the last think. If the server is rendering frames slower than 100/s, then it's going to get called less times than that. Then there's the fact that you can actually adjust my timer; I picked 0.01 seconds as an arbitrary number because it works well on surf servers and gives enough time for most collisions to be detected and prevented. You can change it if you want, but I don't see any option to do that with client_PreThink or ShouldCollide.

The only thing I can really give you is that I never really thought of using ShouldCollide before. I'll consider applying that to one of the versions listed but I don't see it as a very good improvement, especially since it's far less efficient than just blocking the touch only when it happens using register_touch or FM_Touch as precision isn't as important in a non-surf server environment.
User avatar
Archive-Poster
Posts: 1229
Gender:
United States of America

Re: [TUT] Semiclip

Post by Archive-Poster »

I did some tests to see how many times a second ShouldCollide and PlayerPreThink gets called every second with 3 bots and myself in the server.

ShouldCollide averaged 700-2000 times a second for 4 players.

PlayerPreThink averaged 300-360 times a second for 4 players.

Both functions would take less than 1ms to compute so I wasn't able to get accurate speed results with tickcount(). ShouldCollide ran more cause it covered each player combination. PreThink requires a loop of all players. I myself would prefer PreThink because it was far more stable and wouldn't jump around.

I skipped using the first poster's info_target think method because I wasn't able to get it to work, and it would most-likely run just as much as PreThink would on a per player basis except requiring 2 nested for loops.
Post Reply

Who is online

Users browsing this forum: No registered users and 2 guests