SinglePlayer is the default GameRule in CRYENGINE. It is designed to be used for player vs AI gameplay.
Here you'll find the example Singleplayer.xml script that is provided with the SDK. Below it you'll find information on what each of these sections mean.
<Mode name='SinglePlayer'>
<!-- Modules want to be switched with SP versions at some point -->
<StatsRecording class="StandardStatsRecording" />
<Spawning class='SpawningBase' teamGame='1' teamSpawnsType='None' isHQSpawningCompatible='0' />
<DamageHandling class='SPDamageHandling' >
<MercyTimeFilters path='Scripts/GameRules/SPMercyTimeFilters.xml' />
</DamageHandling>
</Mode>
--------------------------------------------------------------------------
-- Crytek Source File.
-- Copyright (C), Crytek Studios, 2001-2004.
--------------------------------------------------------------------------
-- $Id$
-- $DateTime$
-- Description: GameRules implementation for Death Match
--
--------------------------------------------------------------------------
-- History:
-- - 22/ 9/2004 16:20 : Created by Mathieu Pinard
-- - 04/10/2004 10:43 : Modified by Craig Tiller
-- - 07/10/2004 16:02 : Modified by Marcio Martins
--
--------------------------------------------------------------------------
SinglePlayer = {
tempVec = {x=0,y=0,z=0},
Client = {},
Server = {},
-- this table is used to track the available entities where we can spawn the
-- player
spawns = {},
}
usableEntityList = {}
if (not g_dmgMult) then g_dmgMult = 1.0; end
----------------------------------------------------------------------------------------------------
function SinglePlayer:OnReset(toGame)
AIReset();
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:EquipActor(actor)
--Log(">> SinglePlayer:EquipActor(%s) <<", actor:GetName());
if(self.game:IsDemoMode() ~= 0) then -- don't equip actors in demo playback mode, only use existing items
--Log("Don't Equip : DemoMode");
return;
end;
if (actor.inventory) then
actor.inventory:Destroy();
end
if (actor.actor and actor.actor:IsPlayer()) then
ItemSystem.GiveItemPack(actor.id, "Player_Default", false, true);
end
if (actor.actor and not actor.actor:IsPlayer()) then
if (actor.Properties) then
local equipmentPack=actor.Properties.equip_EquipmentPack;
if (equipmentPack and equipmentPack ~= "") then
ItemSystem.GiveItemPack(actor.id, equipmentPack, false, false);
if (actor.AssignPrimaryWeapon) then
actor:AssignPrimaryWeapon();
end
end
if(not actor.bGunReady) then
actor.actor:HolsterItem(true);
end
end
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:OnShoot(shooter)
if (shooter and shooter.OnShoot) then
if (not shooter:OnShoot()) then
return false;
end
end
return true;
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:IsUsable(srcId, objId)
if not objId then return 0 end;
local obj = System.GetEntity(objId);
if (obj.IsUsable) then
if (obj:IsHidden()) then
return 0;
end;
local src = System.GetEntity(srcId);
if (src and src.actor and (src:IsDead() or (src.actor:GetSpectatorMode()~=0))) then
return 0;
end
return obj:IsUsable(src);
end
return 0;
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:AreUsable(source, entities)
if (entities) then
for i,entity in ipairs(entities) do
usableEntityList[i] = entity:IsUsable(source);
end
end
return usableEntityList;
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:OnNewUsable(srcId, objId, usableId)
if not srcId then return end
if objId and not System.GetEntity(objId) then objId = nil end
local src = System.GetEntity(srcId)
if src and src.SetOnUseData then
src:SetOnUseData(objId or NULL_ENTITY, usableId)
end
if srcId ~= g_localActorId then return end
if self.UsableMessage then
HUD.SetInstructionObsolete(self.UsableMessage)
self.UsableMessage = nil
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:OnUsableMessage(srcId, objId, objEntityId, usableId)
if srcId ~= g_localActorId then return end
local msg = "";
if objId then
obj = System.GetEntity(objId)
if obj then
if obj.GetUsableMessage then
msg = obj:GetUsableMessage(usableId)
else
local state = obj:GetState()
if state ~= "" then
state = obj[state]
if state.GetUsableMessage then
msg = state.GetUsableMessage(obj, usableId)
end
end
end
end
end
if(UIAction) then
UIAction.StartAction("DisplayUseText", {msg}); --this triggers the UIAction "DisplayUseText" and pass the msg as argument (see FlowGraph UIActions how to send msg to flash)
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:OnLongHover(srcId, objId)
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:EndLevel( params )
if (not System.IsEditor()) then
if (not params.nextlevel) then
Game.PauseGame(true);
Game.ShowMainMenu();
end
g_GameTokenPreviousLevel = GameToken.GetToken( "Game.Global.Previous_Level" );
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:CreateExplosion(shooterIdParam,weaponIdParam,damage,pos,dir,radius,angle,pressure,holesize,effect,effectScale, minRadius, minPhysRadius, physRadius, explosionType, soundRadius)
if (not dir) then
dir=g_Vectors.up;
end
if (not radius) then
radius=5.5;
end
if (not minRadius) then
minRadius=radius/2;
end
if (not physRadius) then
physRadius=radius;
end
if (not minPhysRadius) then
minPhysRadius=physRadius/2;
end
if (not angle) then
angle=0;
end
if (not pressure) then
pressure=200;
end
if (holesize==nil) then
holesize = math.min(radius, 5.0);
end
if (radius == 0) then
return;
end
local shooterId = NULL_ENTITY;
if (shooterIdParam~=0 and shooterIdParam~=nil) then -- 0 is *not* false in lua
shooterId = shooterIdParam;
end
local weaponId = NULL_ENTITY;
if (weaponIdParam~=0 and weaponIdParam~=nil) then
weaponId = weaponIdParam;
end
self.game:ServerExplosion(shooterId, weaponId, damage, pos, dir, radius, angle, pressure, holesize, effect, effectScale, explosionType, minRadius, minPhysRadius, physRadius, soundRadius);
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:CreateHit(targetId,shooterId,weaponId,dmg,radius,material,partId,type,pos,dir,normal)
if (not radius) then
radius=0;
end
local materialId=0;
if (material) then
materialId=self.game:GetHitMaterialId(material);
end
if (not partId) then
partId=-1;
end
local typeId=0;
if (type) then
typeId=self.game:GetHitTypeId(type);
else
typeId=self.game:GetHitTypeId("normal");
end
self.game:ServerHit(targetId, shooterId, weaponId, dmg, radius, materialId, partId, typeId, pos, dir, normal);
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:ClientViewShake(pos, distance, radiusMin, radiusMax, amount, duration, frequency, source, rnd)
if (g_localActor and g_localActor.actor) then
if (distance) then
self:ViewShake(g_localActor, distance, radiusMin, radiusMax, amount, duration, frequency, source, rnd);
return;
end
if (pos) then
local delta = self.tempVec;
CopyVector(delta,pos);
FastDifferenceVectors(delta, delta, g_localActor:GetWorldPos());
local dist = LengthVector(delta);
self:ViewShake(g_localActor, dist, radiusMin, radiusMax, amount, duration, frequency, source, rnd);
return;
end
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:ViewShake(player, distance, radiusMin, radiusMax, amount, duration, frequency, source, rnd)
local deltaDist = radiusMax - distance;
rnd = rnd or 0.0;
if (deltaDist > 0.0) then
local r = math.min(1, deltaDist/(radiusMax-radiusMin));
local amt = amount * r;
local halfDur = duration * 0.5;
player.actor:SetViewShake({x=2*g_Deg2Rad*amt, y=2*g_Deg2Rad*amt, z=2*g_Deg2Rad*amt}, {x=0.02*amt, y=0.02*amt, z=0.02*amt},halfDur + halfDur*r, 1/20, rnd);
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:OnSpawn()
end
----------------------------------------------------------------------------------------------------
function SinglePlayer.Server:OnInit()
self.fallHit={};
self.explosionHit={};
self.collisionHit={};
end
----------------------------------------------------------------------------------------------------
function SinglePlayer.Client:OnInit()
end
----------------------------------------------------------------------------------------------------
function SinglePlayer.Server:OnClientConnect( channelId )
local params =
{
name = "Dude",
class = "Player",
position = {x=0, y=0, z=0},
rotation = {x=0, y=0, z=0},
scale = {x=1, y=1, z=1},
};
player = Actor.CreateActor(channelId, params);
if (not player) then
Log("OnClientConnect: Failed to spawn the player!");
return;
end
local spawnId = self.game:GetFirstSpawnLocation(0);
if (spawnId) then
local spawn=System.GetEntity(spawnId);
if (spawn) then
--set pos
player:SetWorldPos(spawn:GetWorldPos(g_Vectors.temp_v1));
--set angles
spawn:GetAngles(g_Vectors.temp_v1);
g_Vectors.temp_v1.x = 0;
g_Vectors.temp_v1.y = 0;
player.actor:PlayerSetViewAngles(g_Vectors.temp_v1);
spawn:Spawned(player);
return;
end
end
System.Log("$1warning: No spawn points; using default spawn location!")
end
----------------------------------------------------------------------------------------------------
function SinglePlayer.Server:OnClientEnteredGame( channelId, player, loadingSaveGame )
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:GetDamageAbsorption(actor, hit)
return 0
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:CanHitIgnoreInvulnerable(hit, target)
if (self:IsStealthHealthHit(hit.type)) then
return true;
elseif (hit.type == "silentMelee") then
return true;
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:ProcessActorDamage(hit)
local target=hit.target;
local shooter=hit.shooter;
local shooterId = hit.shooterId or NULL_ENTITY;
local weapon=hit.weapon;
local health = target.actor:GetHealth();
if (target.IsInvulnerable and target:IsInvulnerable() and not self:CanHitIgnoreInvulnerable(hit,target))then
return (health <= 0);
end;
local isMultiplayer = self.game:IsMultiplayer();
local totalDamage = g_dmgMult * hit.damage;
local splayer = shooter and shooter.actor and shooter.actor:IsPlayer();
local tplayer = target and target.actor and target.actor:IsPlayer();
if (not isMultiplayer) then
local sai=(not splayer) and shooter and shooter.actor;
local tai=(not tplayer) and target and target.actor;
local dmgMult = 1.0;
if (tplayer) then
dmgMult = g_dmgMult;
end
if (shooter and shooter.actor and tai) then
-- Make the target AI alarmed
AI.SetAlarmed(target.id);
end
if(AI) then
if (sai and not tai) then
-- AI vs. player
totalDamage = AI.ProcessBalancedDamage(shooterId, target.id, dmgMult*hit.damage, hit.type);
totalDamage = totalDamage*(1-self:GetDamageAbsorption(target, hit));
--totalDamage = dmgMult*hit.damage*(1-target:GetDamageAbsorption(hit.type, hit.damage));
elseif (sai and tai) then
-- AI vs. AI
totalDamage = AI.ProcessBalancedDamage(shooterId, target.id, dmgMult*hit.damage, hit.type);
totalDamage = totalDamage*(1-self:GetDamageAbsorption(target, hit));
else
totalDamage = dmgMult*hit.damage*(1-self:GetDamageAbsorption(target, hit));
end
else
totalDamage = dmgMult*hit.damage*(1-self:GetDamageAbsorption(target, hit));
end
end
if (tplayer and (hit.damage > 0) and (hit.type == "collision")) then
if (hit.velocity and hit.velocity > 0.5) then
totalDamage = 0;
end
end
--update the health
local newhealth = math.floor(health - totalDamage);
local useMercyTime = not isMultiplayer and (hit.type ~= "fall") and (hit.type ~= "punish") and (hit.type ~= "vehicleDestruction");
if (tplayer and useMercyTime) then
local threshold=target.actor:GetLowHealthThreshold();
if (health>threshold and newhealth<=0) then
--Log("Prevented %s's one-shot!", target:GetName())
newhealth=math.floor(threshold*0.5);
end
end
-- For boss fights and such it is sometimes desirable that the health
-- cannot drop below a certain minimum threshold (but never ignore
-- custom kill events from FlowGraph and stuff!)
if (hit.type ~= "event") then
if (target.GetForcedMinHealthThreshold) then
local forcedMinHealth = target:GetForcedMinHealthThreshold()
if (newhealth < forcedMinHealth) then
newhealth = forcedMinHealth
end
end
end
health = newhealth;
--keep debug features commented out if not frequently used (useless c++ call in release)
--if (self.game:DebugCollisionDamage()>0) then
-- Log("<%s> hit damage: %d // absorbed: %d // health: %d", target:GetName(), hit.damage, hit.damage*self:GetDamageAbsorption(target, hit), health);
--end
-- Check for player god / demi-god death
if((not isMultiplayer) and (target.id == g_localActorId) and (health <= 0)) then
self.game:DemiGodDeath(); -- Attemp demi-god death if enabled (SP only internally)
local isGod = target.actor:IsGod();
if (isGod and isGod > 0) then
target.actor:SetHealth(0); --is only called to count deaths in GOD mode within C++
health = target.Properties.Damage.health;
end
end
target.actor:SetHealth(health);
health = target.actor:GetHealth();
if (not isMultiplayer) then
-- Death wall bloodsplats are a prototype feature: Only supported on SinglePlayer for now
-- Enabling it on Multiplayer will involve placing this call in an area executed in both server
-- and client sides, where we would have the final damage caused by the hit and can tell if
-- the actor is dead or not (harder than it seems :P)
target:WallBloodSplat(hit, health <= 0);
end
local weaponId = (weapon and weapon.id) or NULL_ENTITY;
local projectileId = hit.projectileId or NULL_ENTITY;
target.actor:DamageInfo(shooterId, target.id, weaponId, projectileId, totalDamage, hit.typeId, hit.dir);
-- feedback the information about the hit to the AI system.
if (not isMultiplayer and AI) then
if(hit.material_type) then
AI.DebugReportHitDamage(target.id, shooterId, totalDamage, hit.material_type);
else
AI.DebugReportHitDamage(target.id, shooterId, totalDamage, "");
end
end
return (health <= 0);
end
----------------------------------------------------------------------------------------------------
function SinglePlayer.Server:OnStartLevel()
CryAction.SendGameplayEvent(NULL_ENTITY, eGE_GameStarted);
if (g_GameTokenPreviousLevel) then
GameToken.SetToken( "Game.Global.Previous_Level", g_GameTokenPreviousLevel );
g_GameTokenPreviousLevel = nil;
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer.Client:OnStartLevel()
end
----------------------------------------------------------------------------------------------------
function SinglePlayer.Client:OnHit(hit)
local trg = hit.target;
-- send hit to target
if (trg and (not hit.backface) and trg.Client and trg.Client.OnHit) then
trg.Client.OnHit(trg, hit);
end
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:PrecacheLevel()
end
----------------------------------------------------------------------------------------------------
function SinglePlayer:IsStealthHealthHit(hitType)
return (hitType == "stealthKill") or (hitType == "stealthKill_Maximum");
end