Posted by: [ICR]
Date posted: Dec 23 2003 User Rating: 4.7 out of 5.0 | Number of views: 10382 Number of comments: 6 | Description: How to implement call of Duty/Halo style weapon slots and switching. |
This code was written for the mod I am coding for, http://www.hl-isolation.com. Basically it adds Call of Duty or Halo style weapon switching. For those of you who haven't played either of these games (and where have you been? Halo is one of the most popular games and Call of Duty is kick arse) basically the player has a certain number of slots, corresponding to weapon types, melee, primary weapon, secondary weapon etc. and they can only have a certain number of weapons in each slot. To pick up a weapon, you must walk over a weapon. If the slots are already full, you can press use to switch the weapon with the one in the slot. This tutorial is limited to one weapon per slot; however, it can be extended to have several weapons. The tutorial will provide hopefully fairly detailed and useful comments, both inside the code and outside, to help you to understand, customize and expand the code.
Firstly we need to set up some definitions to store various values. We need an array to store pointers to the weapons in the slots. We could loop through the player's weapons, however this method is easier to maintain and makes the code smaller. So in the definition of CBasePlayer in player.h add:
| | CBasePlayerWeapon *m_pSlots[4]; |
However, in order to use CBasePlayerWeapon, we need to include the weapon.h with:
Now we have these defined, we need to change the weapon pick up code. For this tutorial we will just be doing it for CBasePlayerWeapon. However, this could be extended to CBasePlayerItem and be used for all manner of items and pickups. So we need to find CBasePlayerWeapon::AddToPlayer in weapons.cpp and add to the beginning:
| | if (pPlayer->m_pSlots[iSlot()]) { if ( !(pPlayer->m_afButtonPressed & IN_USE) ) return FALSE; } |
Then edit the end of the function to look like:
| | if (bResult) { pPlayer->m_pSlots[iSlot()] = (CBasePlayerWeapon *)this; return AddWeapon( ); } return FALSE; |
This checks to see if the slot is occupied. If it is, then the player must hold down use in order to pick up the weapon. Then, at the end, the pointer to the weapon picked up is added into the array, ready for checking next time they walk over a weapon. Inside the if (pPlayer->m_pSlots[iSlot()]) statement, after the existing code, add:
| | pPlayer->pev->weapons &= ~(1<<pPlayer->m_pSlots[iSlot()]->m_iId); |
This is the beginning of the code to remove the weapon from the player. This removes it from the HUD. Then inside the if (bResult) statement above the existing code add:
| | if (pPlayer->m_pSlots[iSlot()]) { pPlayer->m_pSlots[iSlot()]->DestroyItem(); } |
This checks if there is a weapon already in the slot, and if there is it destroys it. This effectively finishes off removing the weapon from the player. The next thing to do is create the new weapon. Inside the if (pPlayer->m_pSlots[iSlot()]) statement, after the existing code, add:
| | CBasePlayerWeapon *DropPointer = (CBasePlayerWeapon *)CBaseEntity::Create( (char *)STRING(pPlayer->m_pSlots[iSlot()]->pev->classname), pPlayer->pev->origin, Vector(0, 0,0) ); if ( DropPointer ) { DropPointer->pev->velocity = pPlayer->pev->velocity; DropPointer->pev->avelocity = Vector ( 0, RANDOM_FLOAT( 0, 100 ), 0 ); DropPointer->SetThink( RetouchThink ); DropPointer->SetTouch( NULL ); DropPointer->pev->nextthink = gpGlobals->time + 3.0; DropPointer->pev->solid = SOLID_NOT; DropPointer->m_iDefaultAmmo = pPlayer->m_pSlots[iSlot()]->m_iClip; } else { ALERT ( at_debug, "Could not create dropped weapon\n" ); } |
There is quite a lot of stuff in here, indeed it is the largest single part of the tutorial. It begins by creating the weapon, using the DropPointer we defined at the beginning. It then checks if this was created, since if it was not created successfully and then we attempted to set its properties, more errors would arise. The velocity is inherited off the player, so if they are running it will look more convincing as it is dropped forward a little. If you want a counter-strike style throw, you can add the relevant vector here. It then sets the touch function to NULL, meaning you cannot pick it up, and the think function to one we will define and write in a little while, which will reset it so you can pick it up. This delay is for two reasons. Firstly, if there was no delay, as the use key is held down for about a second the code will loop, dropping and picking up weapons. This will result in a not enough edicts error and crash the game. Secondly, it allows us to add a delay before the weapon can be picked up, and help prevent "weapon walking" where people continually switch weapons as they walk, allowing them to use two weapons at the same time. The last line sets the amount of ammo the gun holds, so when it is picked up again any ammo that has been used will still remain the same. Okay, the next thing to do is to add the RetouchThink function. Find the declaration of CBasePlayerItem and add:
| | void EXPORT RetouchThink ( void ); |
You may have noticed we use iSlot() a lot to get the slot of the weapon. However, it is not actually defined in the code, so it can be added now. Under the similar functions, add:
| | int iSlot( void ) { return ItemInfoArray[ m_iId ].iSlot; } |
This just provides an easy way to refer to the slot number. Now to add the actual RetouchThink function. Under AddToPlayer in weapons.cpp add:
| | void CBasePlayerItem::RetouchThink( void ) { pev->solid = SOLID_TRIGGER; UTIL_SetOrigin( this, pev->origin ); SetTouch(&CBasePlayerItem::DefaultTouch); SetThink( NULL ); SetNextThink( 0.0 ); } |
This re-links it to the world, makes it solid for pickup again, and resets the touch function. This means it can be touched, and when it is it will call the AddToPlayer function again, ready for use to be pressed and the weapons switched again. All that is left to do is to re-organize the weapons into the relevant slots. They should still all occupy unique positions within the slots, otherwise the sprites will screw up etc. and things become a lot more complicated. With the removal of the existing weapons in the slots, the slot positions are not obvious anyhow.
Everything should now work. Do not hesitate to modify, expand and change the code, and if you make any significant changes post them up somewhere. However, please do not rip this tutorial off, only post if there are significant changes to the way the code works, or new additions added. Also, do not hesitate to point out errors within the code, or ask for more help or explanation. |
|
User Comments
Showing comments 1-6
ICR this one is great! :D But I've found a little bug; if you stand ontop of a weapon and press use while facing in any direction - even upwards, it let's you switch weapons. It would be nice if it was possible only to do so when aiming at the weapon. Maybe it could be done somewhat like the thing with buttons, where you have to aim at them.. Not sure though - you should try it. |
|
Umm... You could make it so that the player's USE key was pressed, it does a traceline to find the entity infront of the player, and if it is a weapon, it picks it up... You could make it draw TRIAPI corners around it's bounding box, to achieve a similar effect to the one in Deus Ex...Edited by Bulk on Dec 27 2003, 22:29:59
|
|
Yes, that's something I didn't think of. Here is the code ripped from the Spirit trigger_onsight entity. If you add this function: // by the criteria we're using, can the Looker see the Seen entity? BOOL CBasePlayerWeapon :: CanSee(CBaseEntity *pLooker, CBaseEntity *pSeen) { // out of range? if (pev->frags && (pLooker->pev->origin - pSeen->pev->origin).Length() > pev->frags) return FALSE;
// check FOV if appropriate if (pev->max_health < 360) { // copied from CBaseMonster's FInViewCone function Vector2D vec2LOS; float flDot; float flComp = pev->health; UTIL_MakeVectors ( pLooker->pev->angles ); vec2LOS = ( pSeen->pev->origin - pLooker->pev->origin ).Make2D(); vec2LOS = vec2LOS.Normalize(); flDot = DotProduct (vec2LOS , gpGlobals->v_forward.Make2D() );
// ALERT(at_debug, "flDot is %f\n", flDot);
if ( pev->max_health == -1 ) { CBaseMonster *pMonst = pLooker->MyMonsterPointer(); if (pMonst) flComp = pMonst->m_flFieldOfView; else return FALSE; // not a monster, can't use M-M-M-MonsterVision }
// outside field of view if (flDot <= flComp) return FALSE; }
// check LOS if appropriate if (!FBitSet(pev->spawnflags, SF_ONSIGHT_NOLOS)) { TraceResult tr; if (SF_ONSIGHT_NOGLASS) UTIL_TraceLine( pLooker->EyePosition(), pSeen->pev->origin, ignore_monsters, ignore_glass, pLooker->edict(), &tr ); else UTIL_TraceLine( pLooker->EyePosition(), pSeen->pev->origin, ignore_monsters, dont_ignore_glass, pLooker->edict(), &tr ); if (tr.flFraction < 1.0 && tr.pHit != pSeen->edict()) return FALSE; }
return TRUE; } Then change the if ( !(pPlayer->m_afButtonPressed & IN_USE) ) line to: if ( !(pPlayer->m_afButtonPressed & IN_USE) || !CanSee( (CBaseEntity *)pPlayer, this) ) Should work I think. I don't think thats the right way to change the pPlayer, I'm busy and hungover atm. Perhaps someone else can post it.
As for Tri-API boxes and stuff, that's not really part of the tutorials objective, but it is a good idea. For my mod I have a Tri-API box (just the corners) drawn round it and the lighting value is changed to make it brighter when you look at it. This basicaly uses a similar method to the one Xeno said. However, I decided not to demonstrate the pickup only when you can see it method with this, as it is more complex, and if you aren't adding the boxes or lighting stuff, it is simpler and better. |
|
Great tutorial. One problem though which can cause HL to crash!
When a player dies, their m_pSlots[] are not reset, so it causes the server to think a player has a weapon when they really don't, but even worse, when they hit Use to pick up a new weapon it crashes when creating the old one (that isn't there but the server doesn't know because the pointer is still valid)
This is an easy fix.
In CBasePlayer::PlayerDeathThink(void) in the if(HasWeapons()) code block add this code before PackDeadPlayerItems():
// Crash fix by DOOM // We got to reset m_pSlots[] or we end up crashing shit for (int i=0; i < 4; i++) // If you increased the slot size in the above tutorial, change it here too m_pSlots[i] = NULL; |
|
Another small point, you might want to add:
DropPointer->pev->spawnflags |= SF_NORESPAWN;// never respawn
when you are setting all of the other DropPointer properties, otherwise it will respawn after someone picks it up again |
|
2 things, i am new to this so im not sure but with the new sdk i think you need to change:
UTIL_SetOrigin( this, pev->origin ); to UTIL_SetOrigin(pev, pev->origin ); AND SetNextThink( 0.0 ); to pev->nextthink = 0.0;
because i got compile errors from the other code, not sure if that makes logic errors but it compiles
next thing, in relation to the m_pSlots not being set to null. with weapons like grenades and trip mines the weapon entity is destroyed when the ammo is empty, so when you get another one the slot seems full still. to fix this i added under CBasePlayerItem::DestroyItem inside the if ( m_pPlayer ) add the line:
m_pPlayer->m_pSlots[iSlot()] = NULL;
Please correct me if i am wrong as this is the first time i have seen the source. |
|
You must register to post a comment. If you have already registered, you must login.
|
297 Approved Articless
8 Pending Articles
3940 Registered Members
0 People Online (6 guests)
|
|