ResourceManager — Harvestable Resources
The ResourceManager (837 lines in Unturned/Managers/ResourceManager.cs) manages the harvestable resource layer of an Unturned level: trees, rocks, bushes, and forageable plants. Unlike the building managers, resources are not player-placed — they are part of the level's terrain decoration system, controlled by LevelGround and instantiated as ResourceSpawnpoint instances at load time.
Resources occupy a unique position in the manager hierarchy. They are owned by LevelGround (which stores them in a RegionDictionaryResourceSpawnpoint), queried through ResourceManager for region-based spatial lookups, damaged through ResourceManager.damage or ResourceManager.forage, and their death/alive state is synchronized across the network. The respawn timer is managed per-resource by the checkCanReset method on ResourceSpawnpoint, driven by the asset's reset field.
Source file:
Unturned/Managers/ResourceManager.cs(837 lines). Supporting type:Unturned/Level/ResourceSpawnpoint.cs(603 lines),Unturned/Level/LevelGround.cs(1601 lines).
Who this article is for
This article is for plugin developers who need to damage, harvest, or track the state of resource objects. It assumes familiarity with the Regions coordinate system and the ResourceAsset / ResourceSpawnpoint types.
What you'll learn
- The region-based resource storage model and spatial queries
- The resource damage pipeline: damage application, tree felling debris physics, reward drops
- The forage system for instant-gather plants
- The respawn timer and reset multiplier system
- The explosion damage integration via
IExplosionDamageable - Network synchronization of alive/dead state per region
Region-based storage
Resources are stored in a RegionDictionaryResourceSpawnpoint managed by LevelGround:
csharp
private static RegionDictionaryResourceSpawnpoint _regionTrees;This dictionary maps region coordinates (Vector2Int) to lists of ResourceSpawnpoint instances. The RESOURCE_REGIONS constant (3 by default, 16 in the BEAUTIFUL build) controls the region overlap for spatial queries.
The legacy _trees array (ListResourceSpawnpoint[,]) is maintained alongside the dictionary for backward compatibility but is marked obsolete.
Spatial queries
csharp
public static void getResourcesInRadius(Vector3 center, float sqrRadius,
List<RegionCoordinate> search, List<Transform> result)Iterates each region in the search list, gets the tree list from LevelGround.GetTreesOrNullInRegion, and collects alive, damageable transforms within the squared radius.
Full tree enumeration
csharp
public static void GatherAllTrees(ListResourceSpawnpoint results)Appends every tree in every region to the results list. Used for global operations (world reset, arena round cleanup).
Resource damage pipeline
csharp
public static void damage(Transform resource, Vector3 direction, float damage,
float times, float drop, out EPlayerKill kill, out uint xp,
CSteamID instigatorSteamID, EDamageOrigin damageOrigin, bool trackKill)The damage method is the most complex in ResourceManager. Its full pipeline:
- Calculate
totalDamage = (ushort)(damage * times). - Fire
onDamageResourceRequestedfor plugin interception. - If cancelled or
totalDamage < 1, return. - Resolve region coordinates from the resource position.
- Linear scan the region's tree list to find the matching
ResourceSpawnpoint. - Check
isDeadandcanBeDamaged. If valid, callregion[index].askDamage(totalDamage). - If the resource dies after damage, execute the death sequence:
Death sequence
When region[index].isDead becomes true:
- Set
kill = EPlayerKill.RESOURCE. - If the asset has an explosion effect, trigger it at the effect spawn position (EffectManager.MEDIUM distance, reliable).
- If the asset is NOT a forageable, process drops:
Reward table path (asset.rewardID != 0)
- Calculate
dropmultiplier from configObjects.Resource_Drops_Multiplier. - Calculate drop direction:
resource.InverseTransformDirection(direction)flattened to XZ, normalized, then transformed back to world space. - Reward count:
CeilToInt(Random.Range(asset.rewardMin, asset.rewardMax + 1) * dropMultiplier), clamped to[0, 100]. - For debris-having assets: drops are positioned along the drop direction with spacing.
- For non-debris assets: drops are scattered within a 2-meter radius.
Legacy path (asset.log != 0 or asset.stick != 0)
- Logs:
CeilToInt(Random.Range(3, 7) * dropMultiplier), dropped along the damage direction. - Sticks:
CeilToInt(Random.Range(2, 5) * dropMultiplier), dropped in a circular scatter pattern.
- Set
xp = asset.rewardXP. - Track the tree kill for nearby players (within 300 meters).
- Call
ServerSetResourceDead(x, y, index, direction * totalDamage).
Forage system
The forage system handles one-hit-harvest resources like berry bushes:
csharp
public static void forage(Transform resource)Client-side, this sends a SendForageRequest with the region coordinates and resource index. The server validates:
- Region coordinates are safe.
- The requesting player is alive.
- The resource index is valid and not dead.
- The resource is within 20 meters of the player.
- The asset has
isForage = true.
On valid forage:
- Apply 1 point of damage (via
askDamage(1)), which kills the resource. - Trigger explosion effect (if configured).
- Resolve the reward item: from
asset.rewardIDspawn table, or fromasset.log. - Add the item directly to the player's inventory (skip ground drop).
- Agriculture skill mastery doubles the harvest (50% chance).
- Send stat update (
EPlayerStat.FOUND_PLANTS). - Pay forage XP via
player.skills.askPay(asset.forageRewardExperience). - Call
ServerSetResourceDead.
Respawn system
checkCanReset
csharp
public bool checkCanReset(float multiplier)
{
return isDead && asset != null && asset.reset > 1
&& Time.realtimeSinceStartup - lastDead > asset.reset * multiplier;
}The reset field from the ResourceAsset controls the respawn delay in seconds. The multiplier parameter allows external scaling (e.g., from game mode config). The resource must have been dead for longer than asset.reset * multiplier seconds.
Global reset
csharp
public static void askClearAllResources()Iterates every region and revives all trees. Used between arena rounds. The per-region askClearRegionResources sends SendClearRegionResources to revive trees in a single region.
ServerSetResourceDead
csharp
private static void ServerSetResourceDead(byte x, byte y, ushort index, Vector3 ragdoll)Broadcasts SendResourceDead to all clients. The client-side ReceiveResourceDead calls regionTrees[index].kill(ragdoll) to trigger the visual death effect (debris spawning, model/stump swap).
ServerSetResourceAlive
SendResourceAlive revives a dead resource on the client side, calling regionTrees[index].revive().
ResourceSpawnpoint internals
The ResourceSpawnpoint class (603 lines) is the most feature-rich spawnpoint type in the SDK. Key fields:
| Field | Type | Purpose |
|---|---|---|
guid | System.Guid | Resource asset GUID |
point | Vector3 | World position |
angle | Quaternion | Rotation |
scale | Vector3 | Scale |
health | ushort | Current health |
isDead | bool (computed) | health == 0 |
lastDead | float | Time.realtimeSinceStartup at death |
asset | ResourceAsset | Resolved asset reference |
model | Transform | The live tree model (or null if dead) |
stump | Transform | The stump model (or null) |
skybox | Transform | The distant LOD model |
canBeDamaged | bool | False if holiday-restricted and holiday not active |
isGenerated | bool | Whether this spawn was procedurally generated |
Tree felling physics
When a tree dies, the kill(Vector3 ragdoll) method on ResourceSpawnpoint handles the visual:
If the asset has debris and debris graphics are enabled:
- Compute a randomized ragdoll force: add 8 to Y, scatter X/Z by ±16, multiply by flight-boost modifier (4x if flying, otherwise 2x).
- Instantiate a debris copy of the tree model (or the dedicated debris prefab) at the model position + vertical offset.
- Add a
Rigidbodywith interpolation, discrete collision detection, drag 1, angular drag 1. - Apply the ragdoll force.
- Destroy the debris after 8 seconds.
- If a stump exists and the asset should ignore stump-debris collision, configure
Physics.IgnoreCollisionbetween them.
For forageable assets: deactivate the "Forage" child transform.
Active region management
Trees have region-based visibility activation:
csharp
internal void SetIsActiveInRegion(bool isActive)
internal void SetIsSkyboxActiveInRegion(bool isActive)The UpdateActive() method determines whether the model, stump, and skybox should be visible based on:
- Is the region active or is cinematic mode active?
- Is the tree alive (show model) or dead (show stump)?
- Are the asset's holiday conditions met?
- Is this a dedicated server (skip visual activation)?
Explosion damage integration
ResourceSpawnpoint implements IExplosionDamageable through its TreeRefComponent:
csharp
internal class TreeRefComponent : MonoBehaviour, IExplosionDamageable, ICraftingTagProviderThe ApplyExplosionDamage method:
- Checks
damageParameters.shouldAffectTrees. - Calculates damage falloff by distance from explosion center.
- Performs a line-of-sight test (blocked by terrain or objects between explosion and tree).
- Calls
ResourceManager.damagewith the calculated damage. - Reports kills and XP back through
damageParameters.
This allows explosions to fell trees with proper line-of-sight occlusion and distance-based damage falloff.
Plugin integration
ResourceManager delegates
| Delegate | Signature | Fires |
|---|---|---|
onDamageResourceRequested | (CSteamID, Transform, ref ushort, ref bool, EDamageOrigin) | Before any resource damage |
Use cases
- Custom damage handling: Intercept
onDamageResourceRequestedto modify damage values or add custom harvest effects. - Forage augmentation: Wrap
forageto add custom logic before the resource is consumed. - Respawn scaling: Hook into the game mode config to adjust the multiplier passed to
checkCanReset. - Resource tracking: Monitor
ServerSetResourceDeadandServerSetResourceAlivebroadcasts to track resource state.
Serialization
Resources are serialized as part of LevelGround save data. The LevelGround save includes:
SAVEDATA_TREES_VERSIONtree data format (version 8 adds rotation and scale)- Per-resource: GUID, position, rotation, scale, generated flag
- Legacy support for ID-based loading with automatic GUID migration
The treesHash property provides a hash of the Trees.dat file for integrity checking.
ResourceSpawnpoint — Full lifecycle
Construction and tree instantiation
The ResourceSpawnpoint constructor (the full GUID-taking overload at line 428 of ResourceSpawnpoint.cs) performs the complete tree instantiation:
GUID resolution: If a GUID is provided, resolve the
ResourceAssetviaAssets.find(guid). If only a legacy ID is available, resolve viaAssets.find(EAssetType.RESOURCE, id)and auto-upgrade the GUID.Asset integrity: If running on a non-dedicated server, queue a
ClientAssetIntegrityrequest for the tree's GUID. If missing, register withServerAddKnownMissingAssetto prevent client kicks.Model instantiation: If the asset has a
modelGameObject, instantiate it atpoint + (Vector3.up * scale.y * asset.verticalOffset)with the configured rotation and scale. The model receives aTreeRefComponentfor explosion damage integration.Forageable setup: If the asset is a forageable and not in the editor, add
InteractableForageto the "Forage" child transform.Skybox instantiation: If the asset has a
skyboxGameObject, create the skybox LOD copy with a rotated orientation and the asset's skybox material.Stump instantiation: If the asset has a
stumpGameObject, create the stump at the same position.Holiday restrictions: If the asset has a holiday restriction (
asset.holidayRestriction != ENPCHoliday.NONE), setareConditionsMetbased onHolidayUtil.isHolidayActive.Initial active state: Call
UpdateActive()which determines whether model, stump, skybox should be visible.
Respawn timer
csharp
public bool checkCanReset(float multiplier)
{
return isDead && asset != null && asset.reset > 1
&& Time.realtimeSinceStartup - lastDead > asset.reset * multiplier;
}The lastDead timestamp is set when kill() is called. The reset value from the ResourceAsset defines the base respawn delay in seconds. The multiplier allows game-mode-specific scaling (e.g., faster respawn in arena mode).
Tree kill visual effect
When kill() is called, the debris spawning logic is:
- Calculate ragdoll force:
ragdoll.y += 8; ragdoll.x/z += Random.Range(-16, 16); ragdoll *= (flight boost ? 4 : 2). - Find or create the debris prefab (uses
asset.debrisGameObjectif configured, otherwiseasset.modelGameObject). - Instantiate the debris at
model.position + model.up * asset.DebrisVerticalOffset. - Configure the rigidbody:
Interpolate,Discretecollision detection, drag/angular drag = 1. - Apply the ragdoll force via
AddForce(ragdoll). - Set 8-second auto-destruction via
GameObject.Destroy(gib.gameObject, 8f). - If a stump exists and
asset.ShouldIgnoreCollisionBetweenStumpAndDebris, callPhysics.IgnoreCollisionbetween the debris and stump colliders.
Active state visibility rules
The UpdateActive() method uses a priority-based visibility system:
bool isActiveOrCinematic = isActiveInRegion || GraphicsSettings.WantsCinematicMode;
bool shouldModelBeVisible = isAlive;
bool shouldStumpBeVisible = !isAlive;
if (asset != null && asset.isForage)
{
shouldModelBeVisible = true;
model?.Find("Forage")?.gameObject.SetActive(isAlive);
}
bool shouldBeActive = areConditionsMet && (isDedicatedServer || isActiveOrCinematic);
model?.gameObject.SetActive(shouldBeActive && shouldModelBeVisible);
stump?.gameObject.SetActive(shouldBeActive && shouldStumpBeVisible);For forageable assets, the model is always visible but the "Forage" child (the harvestable part) is only visible when alive.
Skybox LOD visibility
The UpdateSkyboxActive() method controls distant visibility:
csharp
if (skybox != null)
{
bool isLandmarkQualityMet = GraphicsSettings.landmarkQuality >= EGraphicQuality.MEDIUM
&& !GraphicsSettings.WantsCinematicMode;
skybox.gameObject.SetActive(!isActiveInRegion && isSkyboxActiveInRegion
&& isLandmarkQualityMet && areConditionsMet && isAlive);
}Skybox trees are only shown when:
- The main tree is not in the active region (i.e., it's far enough away for the LOD)
- The skybox region is active
- Landmark quality is at least MEDIUM
- Cinematic mode is off
- Holiday conditions are met
- The tree is alive
LevelGround resource loading
Trees.dat loading
LevelGround.loadTrees() reads Terrain/Resources.dat:
csharp
// Version with rotation and scale (SAVEDATA_TREES_VERSION_ROTATION_AND_SCALE = 8)
byte treeCount = river.readByte();
for (int index = 0; index < treeCount; index++)
{
System.Guid guid = river.readGUID();
Vector3 position = river.readSingleVector3();
Quaternion rotation = river.readQuaternion(); // Version >= 8
Vector3 scale = river.readSingleVector3(); // Version >= 8
bool isGenerated = river.readBoolean();
addSpawn(position, rotation, scale, guid, isGenerated);
}Legacy versions (before version 8) stored only ID, position, and generated flag without rotation or scale.
RegionDictionary[T] tree access
csharp
public static ListResourceSpawnpoint GetTreesOrNullInRegion(Vector2Int coord)
{
return _regionTrees?.GetListOrNull(coord);
}The RegionDictionaryResourceSpawnpoint provides region-keyed access with automatic list creation and cleanup:
GetOrAddList(coord): Creates a new list if none exists for this region.GetListOrNull(coord): Returns null if no trees are in this region.ReleaseListIfEmpty(coord): Removes the list entry when the last tree is removed.
Tree count tracking
csharp
private static int _total;
public static int total => _total;The _total field is incremented on addSpawn and must be manually maintained by consumers.
Foliage bake pre-processing
When the landscape foliage system bakes a tile, generated trees within the tile bounds are removed. This prevents double-rendering of procedurally placed trees and foliage:
csharp
if (bakeSettings.bakeResources)
{
Bounds worldBounds = foliageTile.worldBounds;
RegionBoundsInt bounds = Regions.GetCoordinateBoundsInt(worldBounds);
foreach (Vector2Int coord in bounds)
{
ListResourceSpawnpoint trees = GetTreesOrNullInRegion(coord);
// ... remove generated trees within worldBounds
}
}ResourceAsset fields used by ResourceManager
The ResourceAsset fields referenced by ResourceManager.damage and ResourceSpawnpoint:
| Field | Type | Purpose |
|---|---|---|
health | ushort | Starting health |
reset | float | Respawn delay in seconds |
isForage | bool | Whether this resource is one-hit-harvestable |
rewardID | ushort | Spawn table ID for rewards |
rewardMin / rewardMax | int | Reward drop count range |
rewardXP | uint | Experience awarded on kill |
log | ushort | Item ID for log drops (legacy) |
stick | ushort | Item ID for stick drops (legacy) |
hasDebris | bool | Whether felling spawns a debris physics object |
verticalOffset | float | Vertical offset for model placement |
DebrisVerticalOffset | float | Vertical offset for debris spawning |
ExplosionEffectFlags | flags | Determines explosion position/rotation behavior |
holidayRestriction | ENPCHoliday | Seasonal availability |
skyboxGameObject / stumpGameObject | GameObject | LOD and stump prefabs |
Resource region loading at level start
During LevelGround.load(), resource data is loaded from Terrain/Resources.dat:
- The file is opened with
Riverbinary reader. - The version byte determines the format:
- Version >=
SAVEDATA_TREES_VERSION_ROTATION_AND_SCALE (8): reads GUID, position, rotation (Quaternion), scale (Vector3), generated flag. - Earlier versions: reads legacy ID, position, generated flag (rotation defaults to identity, scale defaults to one).
- Version >=
- Each resource is added via
addSpawn(pos, rot, scale, guid, isGenerated). - After loading, trees are registered with the region visibility system.
The treesHash is computed from the Resources.dat file content after loading.
Resource damage edge cases
Protected resources
Resources with canBeDamaged = false cannot be damaged by any means:
csharp
if (!region[index].isDead && region[index].canBeDamaged)The canBeDamaged property returns false when:
- The asset has a
holidayRestrictionset to a non-NONEvalue. - The current active holiday does not match the restriction.
For example, a Christmas tree asset with holidayRestriction = CHRISTMAS will have canBeDamaged = false outside of the Christmas holiday period.
Zero-damage prevention
csharp
if (!shouldAllow || totalDamage < 1)
return;If the onDamageResourceRequested delegate reduces damage below 1, or if the weapon/tool damage calculation results in zero, the damage is silently ignored. This prevents infinite zero-damage hits.
Direction-based drop spawning
The drop direction for reward items is computed from the damage direction:
csharp
Vector3 localDropDirection = resource.InverseTransformDirection(direction);
localDropDirection.y = 0.0f;
localDropDirection.Normalize();
Vector3 dropDirection = resource.TransformDirection(localDropDirection);This transforms the world-space damage direction into the resource's local space, flattens it to the horizontal plane, re-normalizes, and transforms back to world space. The result is a horizontal direction that aligns with the striking direction — logs fly in the direction the tree was hit from.
Debris drop positioning
For resources with asset.hasDebris:
csharp
dropPosition = resource.position + (dropDirection * (2 + reward)) + resource.up * 2f;Each reward item is placed 2 units apart along the drop direction, starting 2 units from the tree center, elevated 2 units above the ground.
For non-debris resources, items are scattered:
csharp
dropPosition = resource.position
+ resource.right * Random.Range(-2.0f, 2.0f)
+ resource.up * 2.0f
+ resource.forward * Random.Range(-2.0f, 2.0f);Forage request validation
The ReceiveForageRequest handler (rate-limited at 10 Hz) performs a thorough validation chain:
- Region coordinates are within safe bounds.
- Player is connected and alive.
- Resource index is within the region's tree list.
- Resource is not already dead.
- Player is within 20 meters of the resource (squared magnitude check).
- Resource asset exists and
isForage == true.
If any check fails, the forage is silently ignored (no refund sent to the player — the item was never consumed).
Resource region repopulation
askClearAllResources — Full world reset
csharp
public static void askClearAllResources()Iterates all WORLD_SIZE × WORLD_SIZE regions and calls askClearRegionResources(x, y) for each. This sends SendClearRegionResources to clients for every region, triggering receiveClearRegionResources which calls tree.revive() on each tree.
This is typically used between arena rounds or when a map-wide resource respawn is needed.
Per-region clear
csharp
public static void askClearRegionResources(byte x, byte y)Server-only. Validates the region coordinates, then broadcasts SendClearRegionResources to all clients. The client calls tree.revive() on every tree in the region, restoring all dead resources to full health.
Plugin integration patterns
Custom resource damage
csharp
ResourceManager.onDamageResourceRequested += (instigator, resource,
ref damage, ref allow, origin) =>
{
// Double damage for a specific resource type
ResourceSpawnpoint spawnpoint = LevelGround.FindResourceSpawnpointByTransform(resource);
if (spawnpoint?.asset?.GUID == mySpecialTreeGuid)
damage *= 2;
};Forage augmentation
The forage method is client-to-server. To add custom behavior before the forage resolves, wrap the ItemConsumeable use skill rather than modifying ResourceManager.
Network synchronization
Region-based initial state
When a client connects, the server sends SendResources for each region. The packet contains:
byte x, y
if (!regions[x, y].isNetworked):
regions[x, y].isNetworked = true
ushort treeCount
for each tree:
ushort assetID
Vector3 position
byte angle
bool isDeadDead/alive RPCs
| RPC | Trigger | Effect |
|---|---|---|
SendResourceDead | Tree killed | Calls regionTrees[index].kill(ragdoll) |
SendResourceAlive | Tree respawned | Calls regionTrees[index].revive() |
SendClearRegionResources | Arena round reset | Calls tree.revive() for all trees in region |
The client-only guard if (!Provider.isServer && !regions[x, y].isNetworked) return prevents processing state updates for regions the client has not yet received the initial state for.
ResourceManager tree initialization state flow
When a ResourceSpawnpoint is constructed, the initialization follows this sequence:
1. GUID resolution: read guid -> resolve asset
2. Asset present?
YES -> set health = asset.health, isAlive = true, areConditionsMet = true
instantiate model prefab at modelPosition
instantiate skybox (if not dedicated server)
instantiate stump
check holiday restrictions
UpdateActive()
NO -> set health = 0, isAlive = false
// Tree will not render, will not be damageable
3. ReturnTrees without assets (missing GUID, missing mod) are created as dead, invisible placeholders. They are not functional in gameplay but preserve the save data structure.
ResourceManager — Dead tree tracking
When a tree dies, _lastDead = Time.realtimeSinceStartup records the time of death. The checkCanReset method uses this to determine when to respawn. Dead trees remain in the region tree list — they are not removed. This preserves the save structure and allows the respawn system to re-use the existing ResourceSpawnpoint.
ResourceAsset types
Resources are divided into two categories by ResourceAsset.isForage:
| Category | isForage | Harvest type | Model behavior |
|---|---|---|---|
| Tree | false | Multi-hit damage | Falls over, debris physics, stump remains |
| Forage | true | One-hit interact | "Forage" child toggles visible/invisible |
Forageables are further defined by their forageRewardExperience and interaction with the agriculture skill mastery.
Example: Damaging a tree and handling the result
csharp
Transform treeTransform = /* from raycast hit on tree collider */;
Vector3 damageDirection = (hit.point - Player.player.transform.position).normalized;
EPlayerKill kill;
uint xp;
ResourceManager.damage(treeTransform, damageDirection,
damageAmount, 1.0f, 1.0f, // damage, times, dropMultiplier
out kill, out xp,
player.channel.owner.playerID.steamID,
EDamageOrigin.Gun);
if (kill == EPlayerKill.RESOURCE)
{
// Tree was felled, xp was awarded
player.skills.askPay(xp);
}Example: Forcing a tree respawn
csharp
// Respawn all trees in a specific region
ResourceManager.askClearRegionResources(x, y);
// Or respawn all trees globally (arena round reset)
ResourceManager.askClearAllResources();Example: Finding a resource by transform
csharp
Transform treeModel = /* transform from raycast */;
ResourceSpawnpoint spawnpoint = LevelGround.FindResourceSpawnpointByTransform(treeModel);
if (spawnpoint != null)
{
ResourceAsset asset = spawnpoint.asset;
ushort health = spawnpoint.health;
float timeUntilRespawn = spawnpoint.lastDead + (asset?.reset ?? 60) - Time.realtimeSinceStartup;
}Resource health and repair
Resource health is stored as a ushort on each ResourceSpawnpoint. The askDamage method applies damage:
csharp
public void askDamage(ushort amount)
{
if (amount == 0 || isDead) return;
if (amount >= health) health = 0;
else health -= amount;
}There is no repair mechanism for resources — once dead, they must respawn naturally via the timer or be revived via askClearAllResources.
Resource collision with barricades and structures
Resources use two collider modes:
- Full collider: Trees with
hasDebrishave a full collider for the standing tree. The collider is disabled when the tree is felled (only the stump remains with collision). - Stump collider: The stump has its own collider that persists after felling.
The Physics.IgnoreCollision between debris and stump prevents the falling tree debris from pushing the stump.
