So you have a large landscape. You put trees on that landscape. You probably put to many trees on that landscape and had to remove some to get performance to an acceptable level. Then you realized you need grass too, that's where this script comes in.
- I created this script to reduce the need for level designers to place large amounts of small foliage when working with large environments. Often having so many grass meshes (even with the foliage painter) can be very expensive when working with large areas.
The Scripts -
NearFoliageManager.uc
Code:
class NearFoliageManager extends Actor;
// Visible/ Collision Components
var const CylinderComponent CylinderComponent;
var const DrawSphereComponent SphereComponent;
// Grass Counts
var array<RuntimeGrass> currentGrass;
var int GrassToSpawnInRing;
var int offsetammount;
// Phys Material
var PhysicalMaterial physMatMask;
// Optimization
var Vector lastSpawnPosition;
var int ReCheckDistance;
var int MaxGrassCount;
simulated function PostBeginPlay() {
super.PostBeginPlay();
Owner.AttachComponent(SphereComponent);
Owner.AttachComponent(CylinderComponent);
}
event Destroyed() {
DestroyAllGrass();
Owner.DetachComponent(SphereComponent);
Owner.DetachComponent(CylinderComponent);
super.Destroyed();
}
event Tick( float DeltaTime ) {
local RuntimeGrass localGrass;
super.Tick(DeltaTime);
// Hacky replication check to ensure destroyed is called.
// Also ensure this only exists on client that owns the pawn.
if (myPawn(Owner).IsAliveAndWell() == false || myPawn(Owner).Controller == none) Destroy();
// Only do both of these either on the client or in singleplayer (NO SPAWNED GRASS ON SERVER)
if (WorldInfo.NetMode == NM_Client || WorldInfo.NetMode == NM_Standalone) {
createGrassMesh(GrassToSpawnInRing);
}
if (WorldInfo.NetMode == NM_Client || WorldInfo.NetMode == NM_Standalone) {
foreach currentGrass(localGrass) {
localGrass.checkDistanceFromOwner();
}
}
}
function DestroyAllGrass() {
local RuntimeGrass localGrass;
foreach currentGrass(localGrass) {
localGrass.Destroy();
}
}
function RemoveGrassInstance(RuntimeGrass grassToRemove) {
currentGrass.RemoveItem(grassToRemove);
}
// Creates a RunTimeGrass, if it succeeds the grass is placed into an array, otherwise it returns false.
unreliable client function createGrassMesh(int grassToSpawn) {
local Vector spawnLocation;
local Rotator spawnRotation;
local RuntimeGrass spawnedGrass;
local int i, rad;
local float radian;
// Trace Info
local TraceHitInfo spawnedGrassTraceInfo;
local Vector traceHitPos, traceHitNormal, traceEndPos;
// Optimization :: Stops Spawning Grass if Max Reach
if (currentGrass.Length > MaxGrassCount) return;
// Optimization :: If player hasnt moved more than 100 UUs since last check, don't spawn grass.
if (VSize(lastSpawnPosition - CylinderComponent.GetPosition()) < ReCheckDistance) return;
rad = CylinderComponent.CollisionRadius-(offsetammount*2);
lastSpawnPosition = CylinderComponent.GetPosition();
for (i=0; i < grassToSpawn; i++) {
// Creates the grass in a circle.
radian = i * Pi/(grassToSpawn/ 2);
spawnLocation = CylinderComponent.GetPosition();
spawnLocation.X += rad * Cos(radian);
spawnLocation.Y += rad * Sin(radian);
// Offsets position to make it feel more random
spawnLocation.X += (offsetammount * ((FRand()*2)-1));
spawnLocation.Y += (offsetammount * ((FRand()*2)-1));
spawnLocation.Z += 500; // Allows grass to spawn on ledges/ hills above player
// Trace downwards to get surface/ hit location/ hitnormal
traceEndPos = spawnLocation;
traceEndPos.Z -= 1000;
//DrawDebugLine(spawnLocation, traceEndPos, 128,128,255, true);
Trace(traceHitPos, traceHitNormal, traceEndPos, spawnLocation, false,, spawnedGrassTraceInfo);
// If the physical material is not grass, back out.
if(spawnedGrassTraceInfo.PhysMaterial != physMatMask) goto'end';
// Align grass to surface normal.
spawnLocation = traceHitPos;
spawnRotation = Rotator(normal(traceHitNormal));
spawnRotation.Pitch-= 16384;
// Spawn da grass. If it fails for whatever reason, back out.
spawnedGrass = Spawn(class'RuntimeGrass', self,, spawnLocation,spawnRotation,,true);
if (spawnedGrass != none) currentGrass.AddItem(spawnedGrass);
end:
}
}
DefaultProperties
{
physMatMask = PhysicalMaterial'PhysMaterials.PhysMats.Grass'
MaxGrassCount = 100
ReCheckDistance = 100
GrassToSpawnInRing = 25
offsetammount = 64
Begin Object Class=CylinderComponent NAME=CollisionCylinder LegacyClassName=Trigger_TriggerCylinderComponent_C lass
CollideActors=true
CollisionRadius=+1024
CollisionHeight=+512.000000
HiddenGame=true
End Object
CollisionComponent=CollisionCylinder
CylinderComponent = CollisionCylinder
Components.Add(CollisionCylinder)
/**
Begin Object Class=DrawSphereComponent Name=Sphere
bDrawOnlyIfSelected=false
bDrawWireSphere=true
SphereRadius=1024
SphereSides=32
SphereColor=(R=255,G=32,B=32)
HiddenEditor=false
HiddenGame=false
End Object
SphereComponent = Sphere
Components.Add(Sphere)
**/
CollisionType = COLLIDE_NoCollision
bBlockActors = false
bCollideActors = true
bStatic = false;
bMovable = true;
bNoDelete = false;
bHidden = false;
}
RuntimeGrass
Code:
class RuntimeGrass extends StaticMeshActor
notplaceable;
function checkDistanceFromOwner() {
local CylinderComponent localManagerCollision;
local float GrassDistance;
if (Owner == none) return;
localManagerCollision = NearFoliageManager(Owner).CylinderComponent;
GrassDistance = VSize(Location - localManagerCollision.GetPosition());
if (GrassDistance > localManagerCollision.CollisionRadius) {
NearFoliageManager(Owner).RemoveGrassInstance(self );
Destroy();
}
}
event Tick( float DeltaTime ) {
// If FoliageManager is destroyed whilst this is created some grass can be left over.
// Hacky fix.
if (Owner == none) Destroy();
}
DefaultProperties
{
Begin Object Name=StaticMeshComponent0
StaticMesh=StaticMesh'myGrass.Meshes.GrassTest'
End Object
DrawScale = 2.5
bNoDelete = false
bStatic = false
bMovable = true
}
Add to Pawn Class
Code:
simulated function PostBeginPlay() {
super.PostBeginPlay();
if (myFoliageManager == none) myFoliageManager = Spawn(class'NearFoliageManager', self,,,,,true);
}
function UnPossessed() {
myFoliageManager.Destroy();
super.UnPossessed();
}
You'll also notice that in order to reduce popping I added a scale by distance to the foliage, this isn't perfect as you do obviously notice the grass 'grow', this could be reduced by increasing the distance of your grass spawning. I did this over transparency as it is cheaper. Here is the material that does this -
How To Improve -
- Randomize Scale/ Rotation of spawned grass.
- Random colour variation (Material)
- List of archetypes to spawn for random models (different grass/ small rock/ bushes)
- Include LightEnvironment for non-dynamic lighting.
- Different Physical Material return different spawn types (such as woodland physical material spawns more bushes)
- Density/ Seperation control.
- Max spawn slope angle.
- Optimization! I'm sure better programmers could come up with methods to improve this a lot. I am no programmer

To answer some obvious questions/ statements that will come up -
Q.) Looks like ****.
- With a better material and foliage mesh it would look no different than placing it in the editor (unless you use lightmass).
Q.) No Foliage at a distance!?!
- This system was intended to give a sense of density and movement as the player walked through the grass. For further away foliage personally I'd look at using planes that always face the camera.
Q.) Does it work online?
- The system is designed to be client side only. It works online, but the server, and other clients, have little to no interaction with it.
Have fun!
Bookmarks