OFC I'll share the code (for what that's worth). Credit goes to Ferali and Higor for walking me through trace code.
I'm using four classes:
The first is the mutator. It does all the heavy lifting.
Code: Select all
class GraffitiMod extends Mutator config(BRUT);
var int NumNodes, NavPoint, SprayAttempts;
var bool Initialized, GraffitiAllDone;
var NavigationPoint PNodes[5001];
var config int GraffitiNumber;
replication
{
reliable if ( Role == ROLE_Authority )
GraffitiNumber;
}
function PostBeginPlay()
{
local NavigationPoint NP;
if (Initialized)
return;
Initialized = True;
SprayAttempts = 0;
for (NP = Level.NavigationPointList; NP != None; NP = NP.NextNavigationPoint)
{
if (NP.IsA('Pathnode') && !NP.Region.Zone.bWaterZone)
{
if(NumNodes <= 5000)
{
PNodes[NumNodes] = NP;
NumNodes++;
}
}
}
SetTimer(0.5,True);
}
simulated Function addMutator(mutator M)
{
if(M.class==self.class)
m.destroy();
else super.addMutator(M);
}
function SpawnGraffiti()
{
local int PointCount;
local NavigationPoint NP;
local rotator newRot;
local vector HitLocation, HitNormal, FireDir, X, Y, Z, Origin, StartTrace, EndTrace;
local actor HitActor, Other;
local int i, j, N, M, SpriteX, SpriteY;
local float yOffset, zOffset;
local tag1 SpawnedGraffitiRocket;
NavPoint = int(RandRange(0,NumNodes));
for (NP = Level.NavigationPointList; NP != None; NP = NP.NextNavigationPoint)
{
if (NP.IsA('PathNode'))
{
if (PointCount == NavPoint)
{
newRot.Yaw = Rand(65535); //360 degrees around
newRot.Pitch = Rand(1820); //from 0 to roughly 10 degrees upward angle
newRot.Roll = 0;
FireDir = vector(newRot);
HitActor = Trace(HitLocation, HitNormal, location + FireDir * 1500, Location, false);
if(( HitActor == Level) && ( HitNormal.Z < 0.33 ) && ( HitNormal.Z > -0.33 )) //Allow for some wall lean, about 30 degrees
{
GetAxes( rotator(-HitNormal), X, Y, Z);
Origin = HitLocation + HitNormal;
//Pass 1
StartTrace = Origin;
EndTrace = Origin + Y * 64 + Z * 64;
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
if (Other != none)
return;
else
{
StartTrace = Origin + Y * 64 + Z * 64;
EndTrace = StartTrace + X * 3;
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
if (Other != none)
{
//Pass 2
StartTrace = Origin;
EndTrace = Origin + Y * -64 + Z * 64;
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
if (Other != none)
return;
else
{
StartTrace = Origin + Y * -64 + Z * 64;
EndTrace = StartTrace + X * 3;
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
if (Other != none)
{
//Pass 3
StartTrace = Origin;
EndTrace = Origin + Y * 64 + Z * -64;
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
if (Other != none)
return;
else
{
StartTrace = Origin + Y * 64 + Z * -64;
EndTrace = StartTrace + X * 3;
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
if (Other != none)
{
//Pass4
StartTrace = Origin;
EndTrace = Origin + Y * -64 + Z * -64;
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
if (Other != none)
return;
else
{
StartTrace = Origin + Y * -64 + Z * -64;
EndTrace = StartTrace + X * 3;
Other = Trace(HitLocation, HitNormal, EndTrace, StartTrace, true);
if (Other != none)
SpawnedGraffitiRocket = Spawn(class'tag1',,, Origin,rotator(-HitNormal));
}
} else return;
}
} else return;
}
} else return;
}
} else return;
} PointCount++;
}
}
}
simulated function Timer()
{
local Tagz1 T;
local int Q;
local PathNode PN;
if (Level.Game.bGameEnded) return;
if(!GraffitiAllDone)
{
SprayAttempts +=1;
if (SprayAttempts > 60) //cap number of tries to prevent runaway
GraffitiAllDone = true;
Q=0;
forEach AllActors(class'Tagz1', T) //cap number of sprays for the map
{
if (T !=none)
Q +=1;
if (Q >= GraffitiNumber)
GraffitiAllDone = true;
}
if(!GraffitiAllDone)
SpawnGraffiti();
else
SetTimer(0, False);
}
}
defaultproperties
{
GraffitiAllDone=False
}
Tag1 is the silent rocket subclass that is spawned right beside the selected spot for the graffiti.
Code: Select all
class tag1 extends RocketMk2;
#exec AUDIO IMPORT FILE="Sounds\silence.wav" NAME=silence
var float SmokeRate;
var bool bRing,bHitWater,bWaterStart;
var int NumExtraRockets;
var rockettrail trail;
simulated function Destroyed()
{
if ( Trail != None )
Trail.Destroy();
Super.Destroyed();
}
simulated function PostBeginPlay()
{
Trail = None;
SoundRadius = 0;
SmokeRate = 0;
LightRadius = 0;
}
simulated function Timer()
{
}
auto state Flying
{
simulated function HitWall (vector HitNormal, actor Wall)
{
local GraffitiLight b, q;
local bool AreaClear;
Explode(Location + ExploWallOut * HitNormal, HitNormal);
AreaClear = True;
//Check the local area for existing sprays, skip if there are any
foreach RadiusActors(class 'GraffitiLight', q, 256)
{
if( Q != none)
AreaClear = False;
}
if ((ExplosionDecal != None) && (Level.NetMode != NM_DedicatedServer) && (AreaClear))
{
if (( HitNormal.Z < 0.33 ) && ( HitNormal.Z > -0.33 ))
{
Spawn(ExplosionDecal,self,,Location, rotator(HitNormal));
b = Spawn(class'GraffitiLight',self,,Location + HitNormal*32);
b.remoterole=ROLE_simulatedproxy;
}
}
}
simulated function ZoneChange( Zoneinfo NewZone )
{
}
simulated function ProcessTouch (Actor Other, Vector HitLocation)
{
if ( (Other != instigator) && !Other.IsA('Projectile') )
Explode(HitLocation,Normal(HitLocation-Other.Location));
}
simulated function Explode(vector HitLocation, vector HitNormal)
{
Destroy();
}
function BeginState()
{
local vector Dir;
Dir = vector(Rotation);
Velocity = speed * Dir;
Acceleration = Dir * 50;
PlayAnim( 'Wing', 0.2 );
if (Region.Zone.bWaterZone)
{
bHitWater = True;
Destroy();
}
}
}
defaultproperties
{
speed=100.00
Damage=0.00
MomentumTransfer=1
SpawnSound=Sound'silence'
ImpactSound=Sound'silence'
ExplosionDecal=Class'Tagz1'
LifeSpan=4.0
AmbientSound=Sound'silence'
DrawScale=0.00
AmbientGlow=0
SoundRadius=0
SoundVolume=0
LightType=0
LightBrightness=0
LightRadius=0
}
Tagz1 is the blastmark subclass that is the actual graffiti
Code: Select all
class Tagz1 expands BlastMark;
#exec TEXTURE IMPORT NAME=SprayA FILE=Textures\Graffiti\SprayA.BMP GROUP=Sprays
#exec TEXTURE IMPORT NAME=SprayB FILE=Textures\Graffiti\SprayB.BMP GROUP=Sprays
#exec TEXTURE IMPORT NAME=SprayC FILE=Textures\Graffiti\SprayC.BMP GROUP=Sprays
#exec TEXTURE IMPORT NAME=SprayD FILE=Textures\Graffiti\SprayD.BMP GROUP=Sprays
///etc
var Texture Tag1Tex;
replication
{
reliable if (Role == ROLE_Authority)
Tag1Tex;
}
simulated function PostBeginPlay()
{
local Tagz1 T;
local int TexChoice;
Super.PostBeginPlay();
SetTimer(1.0, false);
TexChoice = rand(4)+1;
switch(TexChoice)
{
case 1:
self.Texture = texture'SprayA';
self.Style = STY_Modulated;
break;
case 2:
self.Texture = texture'SprayB';
self.Style = STY_Modulated;
break;
case 3:
self.Texture = texture'SprayC';
self.Style = STY_Modulated;
break;
case 4:
self.Texture = texture'SprayD';
self.Style = STY_Modulated;
break;
}
}
simulated function DirectionalAttach(vector Dir, vector Norm)
{
SetRotation(rotator(Norm));
Log(Norm);
Log(Dir);
if( AttachDecal(100, Dir) == None)
{
Destroy();
}
}
simulated function AttachToSurface()
{
bAttached = AttachDecal(100, vect(0,0,1)) != None;
}
simulated function Timer()
{
}
defaultproperties
{
Drawscale=1.0
}
and finally graffitilight is a light subclass that is spawned on the graffiti so you can see it in dark spots but it's also used to check so graffiti don't overlap each other.
Code: Select all
// This spawns a small light to illuminate the sprays and it also
// let's me check for existing sprays so they don't overlap.
class GraffitiLight extends Decoration;
defaultproperties
{
bUnlit=True
Physics=PHYS_Flying
bPushable=False
DrawType=DT_Mesh
Mesh=Mesh'CandleM'
bCollideActors=True
bCollideWorld=True
bBlockActors=True
bBlockPlayers=True
CollisionRadius=0.00
CollisionHeight=0.00
bStatic=False
LifeSpan=99999
LightType=LT_Steady
LightRadius=5
LightBrightness=100
LightHue=0
LightSaturation=255
Drawscale=0.00001
}
Now bear in mind that this mod was written with hard checks for all the textures being 128X128. If you want to use other sizes than that you'll need to adjust the tracing. This code will compile, all you need to do is add four textures for it to use. I've been testing this code for a couple of days so if I find improvements I'll update the post. Same thing applies if you see something I've missed.
In practice this works very well. I haven't seen it mess up yet, though it does struggle to find good spots. I have attempts capped at 60 but I'm going to increase that to probably 100 and speed the timer up to 3 times a second. It's running at 2 times and there is no impact at all to the server. A lot of the maps are ending with 60 tries and only one or two graffiti being applied but this is due to the lack of good, flat, and open wall space to use. That's fine, I'd rather it's not applied than done half on a wall.
If you compile this code to release as a mod you are welcome to use whatever name you want. I've included this code into the BRUT mod so there won't be a conflict. Please credit Ferali and Higor if you do, I just assembled their instructions.
-Kelly