new world loading system (#110)

* wip

* much faster world creation

* add StaticGeometryInspector

* disable child/parent logic and fading

* rename

* (de)activate objects based on parent

* set draw distance based on layers

* ...

* wip

* wip

* wip

* remove unused param

* prevent concurrent modification

* ...

* catch exceptions when notifying

* ...

* notify about area, not objects

* limit public access to Area

* ...

* ...

* allow public access

* add public api

* adapt code

* pass callback to ctor

* adapt focus points

* fix

* fix intersection check

* support rectangles

* adjust parameters in prefab

* this should fix IsInsideOf()

* ...

* ...

* fix getting area by index

* create area if not exists

* ...

* ...

* ...

* wip for distance levels

* remove constraint on generic parameter

* add some validation

* fix

* fix draw distance per level

* change time of day in which lights are visible

* add todos

* don't use id for UnRegisterFocusPoint()

* use hash set for storing focus points

* add 1 more level

* mark area for update only if visibility changes

* profile WorldSystem calls

* add some profiling sections

* limit time per frame for LoadingThread

* switch custom concurrent queue

* copy jobs to buffer

* rename

* change max draw distance setting

* wait one more frame

* try to remove 801 distance level to remove holes

* attempt to hide interiors, but failed

* delete no longer needed script

* optimization

* some error checking

* add camera as focus point

* dont add camera as focus point in headless mode

* working on load priority

* fix bug - load priority finished

* ...

* small optimizations

* ...

* ...

* remove unneeded variable

* add fading

* dont do fading in headless mode

* fadeRate available in inspector

* change fade rate

* take into account if geometry is loaded when checking if object should be visible, and if fading should be done

* small optimization

* cache IsInHeadlessMode

* display Instance info in inspector

* move interiors up in the sky

* rename

* adapt code to different y pos of interiors

* refactor

* fix finding matched enex for enexes that lead to the same interior level

* display new world stats

* rename

* rename class

* ...

* ...

* extract function

* extract parameters into a struct

* add focus point to dead body

* add focus point to vehicle

* add focus point to vehicle detached parts

* remove OutOfRangeDestroyer from vehicle, and destroy vehicle if it falls below the map

* dont use focus points on vehicle and vehicle detached parts, when not on server

* add focus point for npc peds

* add possibility to set timeout during which focus point keeps revealing after it's destroyed

* adapt UnRegisterFocusPoint() to timeout

* rename

* adapt code

* cleanup MapObject class

* ...

* converting to `lock()`

* optimize method: use 1 lock instead of 3

* call OnObjectFinishedLoading() instead of AddToLoadedObjects()

* ...

* make sure it's main thread

* AsyncLoader is no longer thread safe

* convert static members to non-static in LoadingThread

* fix

* ...

* store indexes for each area

* impl GetAreaCenter()

* calculate load priority based on distance to area, not objects ; limit time per frame ; sort area in Cell, not in concurrent SortedSet ;

* add support for changing draw distance at runtime

* delay setting the new value by 0.2 s

* have a separate default max draw distance for mobile platforms

* adjust y axis world params so that number of visible areas is reduced

* remove "camera far clip plane" setting

* rename

* document flags

* rename

* disable shadow casting and receiving for some objects

* allow casting shadows for LODs with large draw distance

* remove "WorldSystem" layer

* revert layer
This commit is contained in:
in0finite 2021-07-18 06:03:43 +02:00 committed by GitHub
parent bac100ebcb
commit 32c0be1af2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
68 changed files with 2620 additions and 1110 deletions

View file

@ -48,8 +48,8 @@ MonoBehaviour:
m_EditorClassIdentifier:
serverOnly: 0
localPlayerAuthority: 0
m_AssetId:
m_SceneId: 3719402579
m_AssetId: 14d22e60f4926f64cbd36f393fe11b9a
m_SceneId: 0
--- !u!114 &3458215582039164093
MonoBehaviour:
m_ObjectHideFlags: 0
@ -62,6 +62,10 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 52246fc08ef54f047800aa1a2be91fd2, type: 3}
m_Name:
m_EditorClassIdentifier:
syncInterval: 0.1
focusPointParameters:
hasRevealRadius: 1
revealRadius: 150
--- !u!114 &3928823046603185135
MonoBehaviour:
m_ObjectHideFlags: 0

View file

@ -1,6 +1,6 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &652499336889633433
--- !u!1 &4194179655117615784
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -8,9 +8,9 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3575875094125856578}
- component: {fileID: 3628301541464980759}
- component: {fileID: 8390198767576974155}
- component: {fileID: 187624993197227891}
- component: {fileID: 99305259034019110}
- component: {fileID: 5140126490860469114}
m_Layer: 0
m_Name: box2
m_TagString: Untagged
@ -18,35 +18,35 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &3575875094125856578
--- !u!4 &187624993197227891
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 652499336889633433}
m_GameObject: {fileID: 4194179655117615784}
m_LocalRotation: {x: 0, y: 0, z: -0.17364825, w: 0.9848078}
m_LocalPosition: {x: 0.15, y: 0, z: 0}
m_LocalScale: {x: 0.1, y: 0.7, z: 0.39999998}
m_Children: []
m_Father: {fileID: 8167605406775738267}
m_Father: {fileID: 4783866566622431146}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: -20}
--- !u!33 &3628301541464980759
--- !u!33 &99305259034019110
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 652499336889633433}
m_GameObject: {fileID: 4194179655117615784}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &8390198767576974155
--- !u!23 &5140126490860469114
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 652499336889633433}
m_GameObject: {fileID: 4194179655117615784}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0
@ -77,7 +77,7 @@ MeshRenderer:
m_SortingLayerID: 0
m_SortingLayer: 0
m_SortingOrder: 0
--- !u!1 &2538922681126103069
--- !u!1 &5154345777629047764
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -85,52 +85,52 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7884669128420726770}
- component: {fileID: 7184170559896548561}
- component: {fileID: 808976458662788928}
- component: {fileID: 414177935848565884}
- component: {fileID: 3257317628062351182}
- component: {fileID: 7305951874729311165}
- component: {fileID: 31071295224695786}
- component: {fileID: 2351660952039690332}
m_Layer: 0
m_Name: Enex
m_Name: Logic
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &7884669128420726770
--- !u!4 &3257317628062351182
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2538922681126103069}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_GameObject: {fileID: 5154345777629047764}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children:
- {fileID: 8167605406775738267}
- {fileID: 4783866566622431146}
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!65 &7184170559896548561
--- !u!65 &7305951874729311165
BoxCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2538922681126103069}
m_GameObject: {fileID: 5154345777629047764}
m_Material: {fileID: 0}
m_IsTrigger: 1
m_Enabled: 1
serializedVersion: 2
m_Size: {x: 1, y: 1, z: 1}
m_Center: {x: 0, y: 0, z: 0}
--- !u!54 &808976458662788928
--- !u!54 &31071295224695786
Rigidbody:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2538922681126103069}
m_GameObject: {fileID: 5154345777629047764}
serializedVersion: 2
m_Mass: 0.0000001
m_Drag: 0
@ -140,13 +140,13 @@ Rigidbody:
m_Interpolate: 0
m_Constraints: 0
m_CollisionDetection: 0
--- !u!114 &414177935848565884
--- !u!114 &2351660952039690332
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2538922681126103069}
m_GameObject: {fileID: 5154345777629047764}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d1c9d26ffa1f86c4392cbc4f10792f8d, type: 3}
@ -155,9 +155,9 @@ MonoBehaviour:
Flags: []
minArrowPos: -0.15
maxArrowPos: 0.15
arrowTransform: {fileID: 8167605406775738267}
arrowTransform: {fileID: 4783866566622431146}
arrowMoveSpeed: 0.2
--- !u!1 &4678743238979879263
--- !u!1 &8346472704221690222
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -165,7 +165,7 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8167605406775738267}
- component: {fileID: 4783866566622431146}
m_Layer: 0
m_Name: Graphics
m_TagString: Untagged
@ -173,23 +173,23 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &8167605406775738267
--- !u!4 &4783866566622431146
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4678743238979879263}
m_LocalRotation: {x: 0, y: 0.7071068, z: 0, w: 0.7071068}
m_GameObject: {fileID: 8346472704221690222}
m_LocalRotation: {x: -0, y: 0.7071068, z: -0, w: 0.7071068}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 2, z: 1}
m_Children:
- {fileID: 7155344991509732878}
- {fileID: 3575875094125856578}
m_Father: {fileID: 7884669128420726770}
- {fileID: 5797954369941300799}
- {fileID: 187624993197227891}
m_Father: {fileID: 3257317628062351182}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 90, z: 0}
--- !u!1 &5283615154995685745
--- !u!1 &8820782974559310144
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -197,9 +197,9 @@ GameObject:
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 7155344991509732878}
- component: {fileID: 2310926300048183979}
- component: {fileID: 7210435366399545569}
- component: {fileID: 5797954369941300799}
- component: {fileID: 1380515625963776666}
- component: {fileID: 6280833039519983824}
m_Layer: 0
m_Name: box1
m_TagString: Untagged
@ -207,35 +207,35 @@ GameObject:
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &7155344991509732878
--- !u!4 &5797954369941300799
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5283615154995685745}
m_GameObject: {fileID: 8820782974559310144}
m_LocalRotation: {x: 0, y: 0, z: 0.17364816, w: 0.9848078}
m_LocalPosition: {x: -0.15, y: 0, z: 0}
m_LocalScale: {x: 0.1, y: 0.7, z: 0.4}
m_Children: []
m_Father: {fileID: 8167605406775738267}
m_Father: {fileID: 4783866566622431146}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 20}
--- !u!33 &2310926300048183979
--- !u!33 &1380515625963776666
MeshFilter:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5283615154995685745}
m_GameObject: {fileID: 8820782974559310144}
m_Mesh: {fileID: 10202, guid: 0000000000000000e000000000000000, type: 0}
--- !u!23 &7210435366399545569
--- !u!23 &6280833039519983824
MeshRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5283615154995685745}
m_GameObject: {fileID: 8820782974559310144}
m_Enabled: 1
m_CastShadows: 0
m_ReceiveShadows: 0

View file

@ -34,6 +34,7 @@ GameObject:
- component: {fileID: 4992771638147279046}
- component: {fileID: 2664216849870465946}
- component: {fileID: 3560199336842227889}
- component: {fileID: 7933397811870977063}
m_Layer: 0
m_Name: GameManager
m_TagString: Untagged
@ -156,12 +157,20 @@ MonoBehaviour:
groundFindingIgnoredLayerMask:
serializedVersion: 2
m_Bits: 31286
playerPedFocusPointParameters:
hasRevealRadius: 0
revealRadius: 0
timeToKeepRevealingAfterRemoved: 3
npcPedFocusPointParameters:
hasRevealRadius: 1
revealRadius: 150
timeToKeepRevealingAfterRemoved: 3
cameraDistanceFromPed: 4
minCameraDistanceFromPed: 2
maxCameraDistanceFromPed: 30
cameraRaycastIgnoredLayerMask:
serializedVersion: 2
m_Bits: 31542
m_Bits: 64310
legAndArmDamageMultiplier: 0.8
stomachAndChestDamageMultiplier: 1
headDamageMultiplier: 4
@ -177,8 +186,6 @@ MonoBehaviour:
healthBackgroundColor: {r: 0.5, g: 0, b: 0, a: 1}
AIStoppingDistance: 3
AIVehicleEnterDistance: 1.25
AIOutOfRangeTimeout: 5
AIOutOfRangeDistance: 250
pedSyncRate: 20
ragdollPrefab: {fileID: 2141754834917738923, guid: 14d22e60f4926f64cbd36f393fe11b9a,
type: 3}
@ -215,6 +222,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: be87ebc3b426867448164f659f750671, type: 3}
m_Name:
m_EditorClassIdentifier:
maxTimePerFrameMs: 10
--- !u!114 &114463745687567314
MonoBehaviour:
m_ObjectHideFlags: 0
@ -294,6 +302,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
serverChatNick: <color=green>Server</color>
maxChatMessageLength: 50
--- !u!114 &7442340376164908290
MonoBehaviour:
m_ObjectHideFlags: 0
@ -452,3 +461,17 @@ MonoBehaviour:
pedLimitInterval: 2
weaponLimitInterval: 1
maxVehiclesPerPlayer: 3
--- !u!114 &7933397811870977063
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1297494511425690}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9dd4217c14fd11f4e8b46fe4a95f7aa5, type: 3}
m_Name:
m_EditorClassIdentifier:
_defaultMaxDrawDistance: 1500
_defaultMaxDrawDistanceOnMobile: 1000

View file

@ -0,0 +1,59 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &1450017544071322502
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 6243884556424213097}
- component: {fileID: 4881020080364246716}
- component: {fileID: 2031804573005969323}
m_Layer: 15
m_Name: MapObjectActivator
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &6243884556424213097
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1450017544071322502}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &4881020080364246716
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1450017544071322502}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 38d6efa58345a004eb09a90ed4a28e2f, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!135 &2031804573005969323
SphereCollider:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1450017544071322502}
m_Material: {fileID: 0}
m_IsTrigger: 1
m_Enabled: 1
serializedVersion: 2
m_Radius: 0.00001
m_Center: {x: 0, y: 0, z: 0}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 136804ed85ed64546a9f516463106810
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,45 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &5341989147525447730
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1810686241933207467}
- component: {fileID: 7540295739339550804}
m_Layer: 0
m_Name: StaticGeometry
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1810686241933207467
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5341989147525447730}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &7540295739339550804
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5341989147525447730}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c2400fe08bb4191409f1e53b32fedcc1, type: 3}
m_Name:
m_EditorClassIdentifier:

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f6207237b8bbdaa49b948fa374797093
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -14,6 +14,8 @@ GameObject:
- component: {fileID: 9183683351129218624}
- component: {fileID: 575085982327711637}
- component: {fileID: 6891581410426086992}
- component: {fileID: 1742792654273641741}
- component: {fileID: 9038525027628313851}
m_Layer: 0
m_Name: Vehicle
m_TagString: Untagged
@ -186,3 +188,32 @@ AudioSource:
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
--- !u!114 &1742792654273641741
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5841438317718401360}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8643b1a14caafa547beb3c2e2780085f, type: 3}
m_Name:
m_EditorClassIdentifier:
parameters:
hasRevealRadius: 1
revealRadius: 150
--- !u!114 &9038525027628313851
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5841438317718401360}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6d68c88ae66cbc64c997da0c710b080c, type: 3}
m_Name:
m_EditorClassIdentifier:
componentsToDestroy:
- {fileID: 1742792654273641741}

View file

@ -12,6 +12,8 @@ GameObject:
- component: {fileID: 6750907620125006804}
- component: {fileID: 3359912204732796237}
- component: {fileID: 7107055912798612425}
- component: {fileID: 1658066758197886442}
- component: {fileID: 9051170896248035060}
m_Layer: 13
m_Name: VehicleExplosionLeftoverPart
m_TagString: Untagged
@ -77,3 +79,32 @@ MonoBehaviour:
syncInterval: 0.0667
disableCollisionDetectionOnClients: 1
disableGravityOnClients: 1
--- !u!114 &1658066758197886442
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5998353205578393877}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 8643b1a14caafa547beb3c2e2780085f, type: 3}
m_Name:
m_EditorClassIdentifier:
parameters:
hasRevealRadius: 1
revealRadius: 150
--- !u!114 &9051170896248035060
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5998353205578393877}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6d68c88ae66cbc64c997da0c710b080c, type: 3}
m_Name:
m_EditorClassIdentifier:
componentsToDestroy:
- {fileID: 1658066758197886442}

View file

@ -471,18 +471,30 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
PreviewCamera: {fileID: 0}
focusPoints: []
maxTimeToUpdatePerFrameMs: 10
Water: {fileID: 11455068}
divisionLoadOrderDistanceFactor: 16
divisionRefreshDistanceDelta: 1
divisionsUpdateInterval: 0.3
maxDrawDistance: 500
drawDistancesPerLayers:
- 101
- 301
- 1501
yWorldSystemSize: 7000
yWorldSystemNumAreas: 1
xzWorldSystemNumAreasPerDrawDistanceLevel: 640064006400
interiorHeightOffset: 5000
fadeRate: 4
loadParkedVehicles: 0
enexPrefab: {fileID: 2538922681126103069, guid: 1858efe8eaf422544a4651b2fba2937a,
mapObjectActivatorPrefab: {fileID: 0}
staticGeometryPrefab: {fileID: 5341989147525447730, guid: f6207237b8bbdaa49b948fa374797093,
type: 3}
enexPrefab: {fileID: 5154345777629047764, guid: 1858efe8eaf422544a4651b2fba2937a,
type: 3}
lightSourcePrefab: {fileID: 5315691503008682268, guid: be3276cb0ed03dc4cb44bd04cde4bcfb,
type: 3}
lightScaleMultiplier: 0.5
redTrafficLightDuration: 7
yellowTrafficLightDuration: 2
greenTrafficLightDuration: 7
--- !u!114 &11450014
MonoBehaviour:
m_ObjectHideFlags: 0

View file

@ -125,6 +125,8 @@ GameObject:
- component: {fileID: 1117230322}
- component: {fileID: 1117230321}
- component: {fileID: 1117230325}
- component: {fileID: 1117230326}
- component: {fileID: 1117230327}
m_Layer: 0
m_Name: Camera
m_TagString: MainCamera
@ -221,6 +223,34 @@ MonoBehaviour:
displayGizmos: 0
zOnShader: {fileID: 4800000, guid: 6602491fb85d04f7facf2932f1831720, type: 3}
zOffShader: {fileID: 4800000, guid: 12cb60961770c4d7e8cc1662ca101fec, type: 3}
--- !u!114 &1117230326
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1117230320}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f261590297a9b11459cc24b5b949c608, type: 3}
m_Name:
m_EditorClassIdentifier:
hasRevealRadius: 0
revealRadius: 50
--- !u!114 &1117230327
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1117230320}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4bb8f0fea23c3fe44828518a6ea9ad15, type: 3}
m_Name:
m_EditorClassIdentifier:
componentsToDestroy:
- {fileID: 1117230326}
--- !u!108 &1430544495 stripped
Light:
m_CorrespondingSourceObject: {fileID: 10808534, guid: b6900baf9f552ba46ad27d4ada4d5489,
@ -297,18 +327,6 @@ PrefabInstance:
m_Modification:
m_TransformParent: {fileID: 0}
m_Modifications:
- target: {fileID: 11436500, guid: b6900baf9f552ba46ad27d4ada4d5489, type: 3}
propertyPath: focusPoints.Array.size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 11436500, guid: b6900baf9f552ba46ad27d4ada4d5489, type: 3}
propertyPath: Focus
value:
objectReference: {fileID: 1117230324}
- target: {fileID: 11436500, guid: b6900baf9f552ba46ad27d4ada4d5489, type: 3}
propertyPath: focusPoints.Array.data[0]
value:
objectReference: {fileID: 0}
- target: {fileID: 499186, guid: b6900baf9f552ba46ad27d4ada4d5489, type: 3}
propertyPath: m_LocalPosition.x
value: 0
@ -341,6 +359,18 @@ PrefabInstance:
propertyPath: m_RootOrder
value: 1
objectReference: {fileID: 0}
- target: {fileID: 11436500, guid: b6900baf9f552ba46ad27d4ada4d5489, type: 3}
propertyPath: focusPoints.Array.size
value: 1
objectReference: {fileID: 0}
- target: {fileID: 11436500, guid: b6900baf9f552ba46ad27d4ada4d5489, type: 3}
propertyPath: Focus
value:
objectReference: {fileID: 1117230324}
- target: {fileID: 11436500, guid: b6900baf9f552ba46ad27d4ada4d5489, type: 3}
propertyPath: focusPoints.Array.data[0]
value:
objectReference: {fileID: 0}
- target: {fileID: 11455068, guid: b6900baf9f552ba46ad27d4ada4d5489, type: 3}
propertyPath: WaterPrefab
value:

View file

@ -116,7 +116,6 @@ namespace SanAndreasUnity.Behaviours
AddLoadingStep( new LoadingStep( () => Cell.Instance.InitStaticGeometry (), "Init static geometry", 0.35f ) );
AddLoadingStep( new LoadingStep( () => Cell.Instance.LoadParkedVehicles (), "Loading parked vehicles", 0.2f ) );
AddLoadingStep( new LoadingStep( () => Cell.Instance.CreateEnexes (), "Creating enexes", 0.1f ) );
AddLoadingStep( new LoadingStep( () => Cell.Instance.AddMapObjectsToDivisions (), "Adding map objects to divisions", 0.85f ) );
AddLoadingStep( new LoadingStep( () => Cell.Instance.LoadWater (), "Loading water", 0.08f ) );
AddLoadingStep( new LoadingStep( () => Cell.Instance.FinalizeLoad (), "Finalize world loading", 0.01f ) );
@ -149,7 +148,12 @@ namespace SanAndreasUnity.Behaviours
var stopwatchForSteps = new System.Diagnostics.Stopwatch ();
foreach (var step in m_loadingSteps) {
foreach (var step in m_loadingSteps)
{
// wait some more time before going to next step, because sometimes Unity does something
// in the background at the end of a frame, eg. it updates Collider positions if you changed them
yield return null;
// update description
LoadingStatus = step.Description;

View file

@ -1,41 +1,25 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics;
using SanAndreasUnity.Behaviours.World;
using UnityEngine;
using Debug = UnityEngine.Debug;
using UnityEngine.Profiling;
using Object = UnityEngine.Object;
using Profiler = UnityEngine.Profiling.Profiler;
namespace SanAndreasUnity.Behaviours
{
public abstract class MapObject : MonoBehaviour, IComparable<MapObject>
public abstract class MapObject : MonoBehaviour
{
private static Texture2D _sNoiseTex;
private static bool ShouldGenerateNoiseTex
{
get { return _sNoiseTex == null || _sNoiseTex.width != Screen.width || _sNoiseTex.height != Screen.height; }
}
private static bool ShouldGenerateNoiseTex => _sNoiseTex == null || _sNoiseTex.width != Screen.width || _sNoiseTex.height != Screen.height;
private static int _sBreakableLayer = -1;
public static int BreakableLayer => _sBreakableLayer == -1 ? _sBreakableLayer = LayerMask.NameToLayer("Breakable") : _sBreakableLayer;
public static int BreakableLayer
{
get { return _sBreakableLayer == -1 ? _sBreakableLayer = LayerMask.NameToLayer("Breakable") : _sBreakableLayer; }
}
private static int _sNoiseTexPropertyId = -1;
protected static int NoiseTexPropertyId => _sNoiseTexPropertyId == -1 ? _sNoiseTexPropertyId = Shader.PropertyToID("_NoiseTex") : _sNoiseTexPropertyId;
private static int _sNoiseTexId = -1;
protected static int NoiseTexId
{
get { return _sNoiseTexId == -1 ? _sNoiseTexId = Shader.PropertyToID("_NoiseTex") : _sNoiseTexId; }
}
private static int _sFadeId = -1;
protected static int FadeId
{
get { return _sFadeId == -1 ? _sFadeId = Shader.PropertyToID("_Fade") : _sFadeId; }
}
private static int _sFadePropertyId = -1;
protected static int FadePropertyId => _sFadePropertyId == -1 ? _sFadePropertyId = Shader.PropertyToID("_Fade") : _sFadePropertyId;
private static void GenerateNoiseTex()
{
@ -74,58 +58,68 @@ namespace SanAndreasUnity.Behaviours
}
}
private static readonly System.Random _sRandom = new System.Random(0x54e03b19);
public bool IsVisibleInMapSystem { get; private set; } = false;
public float LoadPriority { get; private set; } = float.PositiveInfinity;
private bool _loaded;
public bool HasLoaded => _loaded;
public bool HasLoaded { get { return _loaded; } }
public Vector3 CachedPosition { get; private set; }
public List<String> Flags;
public Vector2 CellPos { get; private set; }
public int RandomInt { get; private set; }
internal float LoadOrder { get; private set; }
protected static T Create<T>(GameObject prefab)
where T : MapObject
{
GameObject go = Object.Instantiate(prefab, Cell.Instance.transform);
return go.GetComponent<T>();
}
protected void Initialize(Vector3 pos, Quaternion rot)
{
transform.position = pos;
transform.localRotation = rot;
this.transform.position = pos;
this.transform.localRotation = rot;
CellPos = new Vector2(pos.x, pos.z);
RandomInt = _sRandom.Next();
this.CachedPosition = pos;
_loaded = false;
}
public bool RefreshLoadOrder(Vector3 from)
public void SetDrawDistance(float f)
{
Profiler.BeginSample ("MapObject.RefreshLoadOrder", this);
LoadOrder = OnRefreshLoadOrder(from);
Profiler.EndSample ();
return !float.IsPositiveInfinity(LoadOrder);
}
protected abstract float OnRefreshLoadOrder(Vector3 from);
public void Show()
public void Show(float loadPriority)
{
if (this.IsVisibleInMapSystem)
return;
this.IsVisibleInMapSystem = true;
this.LoadPriority = loadPriority;
if (!_loaded)
{
_loaded = true;
Profiler.BeginSample ("OnLoad", this);
OnLoad();
this.OnLoad();
Profiler.EndSample ();
}
Profiler.BeginSample ("OnShow", this);
OnShow();
this.OnShow();
Profiler.EndSample ();
}
LoadOrder = float.PositiveInfinity;
public void UnShow()
{
if (!this.IsVisibleInMapSystem)
return;
this.IsVisibleInMapSystem = false;
this.OnUnShow();
}
protected virtual void OnLoad()
@ -134,11 +128,12 @@ namespace SanAndreasUnity.Behaviours
protected virtual void OnShow()
{
this.gameObject.SetActive(true);
}
public int CompareTo(MapObject other)
protected virtual void OnUnShow()
{
return LoadOrder > other.LoadOrder ? 1 : LoadOrder == other.LoadOrder ? 0 : -1;
this.gameObject.SetActive(false);
}
}
}

View file

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Mirror;
using SanAndreasUnity.Behaviours.World;
using SanAndreasUnity.Importing.Items;
using SanAndreasUnity.Importing.Items.Definitions;
using SanAndreasUnity.Net;
@ -17,6 +18,8 @@ namespace SanAndreasUnity.Behaviours.Peds
public static IEnumerable<DeadBody> DeadBodies => _deadBodies;
public static int NumDeadBodies => _deadBodies.Count;
public FocusPoint.Parameters focusPointParameters = FocusPoint.Parameters.Default;
public PushableByDamage PushableByDamage { get; private set; }
public readonly struct BoneInfo
@ -233,6 +236,8 @@ namespace SanAndreasUnity.Behaviours.Peds
deadBody.m_rigidBodiesDict.Add(pair.Key, new BoneInfo(rb.transform));
}
FocusPoint.Create(ragdollTransform.gameObject, deadBody.focusPointParameters);
deadBody.InitSyncVarsOnServer(ped);
NetManager.Spawn(ragdollGameObject);

View file

@ -115,7 +115,7 @@ namespace SanAndreasUnity.Behaviours
this.raycastDistance = raycastDistance;
}
public static FindGroundParams DefaultBasedOnLoadedWorld => new FindGroundParams((null == Cell.Instance || Cell.Instance.HasExterior));
public static FindGroundParams DefaultBasedOnLoadedWorld => new FindGroundParams((null == Cell.Instance || Cell.Instance.HasMainExterior));
}
@ -173,17 +173,14 @@ namespace SanAndreasUnity.Behaviours
{
if (NetStatus.IsServer)
{
// only register if this ped is owned by some player
if (Player.GetOwningPlayer(this) != null)
this.Cell.focusPoints.AddIfNotPresent(this.transform);
if (this.PlayerOwner != null)
this.Cell.RegisterFocusPoint(this.transform, PedManager.Instance.playerPedFocusPointParameters);
else
this.Cell.RegisterFocusPoint(this.transform, PedManager.Instance.npcPedFocusPointParameters);
}
else if (NetStatus.IsClientActive())
{
// only register if this ped is owned by local player
// TODO: IsControlledByLocalPlayer may not return true, because syncvar in Player script may
// not be updated yet
if (this.IsControlledByLocalPlayer)
this.Cell.focusPoints.AddIfNotPresent(this.transform);
// no need to register on client, because camera will be used as a focus point
}
}
@ -689,9 +686,7 @@ namespace SanAndreasUnity.Behaviours
}
else
{
var matchingEnexes = Importing.Items.Item.Enexes.Where(e => e.Name == enex.Info.Name && e != enex.Info);
Debug.LogFormat("Matching enexes:\n{0}", string.Join("\n", matchingEnexes.Select(e => e.TargetInterior + " - " + e.Flags)));
var counterPart = matchingEnexes.FirstOrDefault(e => e.TargetInterior != enex.Info.TargetInterior);
var counterPart = enex.FindMatchingEnex();
if (counterPart != null)
{
// found a counterpart where we can teleport

View file

@ -32,10 +32,6 @@ namespace SanAndreasUnity.Behaviours
ped.PlayerModel.StartingPedId = def.Id;
ped.EnterVehicleRadius = PedManager.Instance.AIVehicleEnterDistance;
var destroyer = ped.gameObject.GetOrAddComponent<OutOfRangeDestroyer> ();
destroyer.timeUntilDestroyed = PedManager.Instance.AIOutOfRangeTimeout;
destroyer.range = PedManager.Instance.AIOutOfRangeDistance;
if (spawnOnNetwork)
Net.NetManager.Spawn(go);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using SanAndreasUnity.Behaviours.World;
using UnityEngine;
namespace SanAndreasUnity.Behaviours
@ -16,6 +17,9 @@ namespace SanAndreasUnity.Behaviours
public LayerMask groundFindingIgnoredLayerMask = 0;
public FocusPoint.Parameters playerPedFocusPointParameters = new FocusPoint.Parameters(false, 0f, 3f);
public FocusPoint.Parameters npcPedFocusPointParameters = FocusPoint.Parameters.Default;
[Header("Camera")]
public float cameraDistanceFromPed = 3f;
@ -48,8 +52,6 @@ namespace SanAndreasUnity.Behaviours
public float AIStoppingDistance = 3f;
public float AIVehicleEnterDistance = 1.25f;
public float AIOutOfRangeTimeout = 5f;
public float AIOutOfRangeDistance = 250f;
[Header("Net")]

View file

@ -7,7 +7,7 @@ namespace SanAndreasUnity.Behaviours
{
public virtual bool GetSpawnPosition(Player player, out TransformDataStruct transformData)
{
if (null == World.Cell.Instance || World.Cell.Instance.HasExterior)
if (null == World.Cell.Instance || World.Cell.Instance.HasMainExterior)
return SpawnManager.GetSpawnPositionFromFocus(out transformData);
else
return SpawnManager.GetSpawnPositionFromInteriors(out transformData);

View file

@ -108,7 +108,7 @@ namespace SanAndreasUnity.Behaviours
public static bool GetSpawnPositionFromInteriors(out TransformDataStruct transformData)
{
transformData = World.Cell.GetEnexExitTransform(
transformData = World.Cell.Instance.GetEnexExitTransform(
World.Cell.Instance.GetEnexesFromLoadedInteriors()
.ToList()
.RandomElement());

View file

@ -532,6 +532,11 @@ namespace SanAndreasUnity.Behaviours.Vehicles
this.UpdateHighDetailMeshes();
if (Net.NetStatus.IsServer && this.transform.position.y < -2000f)
{
Object.Destroy(this.gameObject);
}
}
private void FixedUpdate()

View file

@ -0,0 +1,42 @@
using SanAndreasUnity.Importing.Items.Placements;
using UnityEngine;
namespace SanAndreasUnity.Behaviours.Vehicles
{
public class VehicleSpawnMapObject : MapObject
{
public static VehicleSpawnMapObject Create(ParkedVehicle info)
{
var vs = new GameObject().AddComponent<VehicleSpawnMapObject>();
vs.Initialize(info);
return vs;
}
public ParkedVehicle Info { get; private set; }
public void Initialize(ParkedVehicle info)
{
Info = info;
name = string.Format("Vehicle Spawn ({0})", info.CarId);
Initialize(info.Position, Quaternion.AngleAxis(info.Angle, Vector3.up));
this.SetDrawDistance(100f);
gameObject.SetActive(false);
gameObject.isStatic = true;
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawCube(transform.position + Vector3.up * 128f, new Vector3(1f, 256f, 1f));
}
protected override void OnLoad()
{
Vehicle.Create(this);
}
}
}

View file

@ -1,55 +0,0 @@
//using Facepunch.Networking;
using SanAndreasUnity.Importing.Items.Placements;
using UnityEngine;
namespace SanAndreasUnity.Behaviours.Vehicles
{
public class VehicleSpawner : MapObject
{
public static VehicleSpawner Create(ParkedVehicle info)
{
//Debug.Log("-333");
var vs = new GameObject().AddComponent<VehicleSpawner>();
vs.Initialize(info);
return vs;
}
public ParkedVehicle Info { get; private set; }
public void Initialize(ParkedVehicle info)
{
Info = info;
name = string.Format("Vehicle Spawner ({0})", info.CarId);
Initialize(info.Position, Quaternion.AngleAxis(info.Angle, Vector3.up));
gameObject.SetActive(false);
gameObject.isStatic = true;
}
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawCube(transform.position + Vector3.up * 128f, new Vector3(1f, 256f, 1f));
}
protected override float OnRefreshLoadOrder(Vector3 from)
{
if (HasLoaded) return float.PositiveInfinity;
var dist = Vector3.Distance(from, transform.position);
if (dist > 100f) return float.PositiveInfinity;
var ray = new Ray(transform.position, Vector3.down);
if (!Physics.Raycast(ray, 2f)) return float.PositiveInfinity;
return dist;
}
protected override void OnLoad()
{
//Debug.Log("-222");
Vehicle.Create(this);
}
}
}

View file

@ -111,7 +111,7 @@ namespace SanAndreasUnity.Behaviours.Vehicles
}
public static Vehicle Create(VehicleSpawner spawner)
public static Vehicle Create(VehicleSpawnMapObject spawner)
{
return Create(spawner.Info.CarId, spawner.Info.Colors, spawner.transform.position,
spawner.transform.rotation);
@ -179,10 +179,6 @@ namespace SanAndreasUnity.Behaviours.Vehicles
inst.transform.position = position - Vector3.up * inst.AverageWheelHeight;
inst.transform.localRotation = rotation;
OutOfRangeDestroyer destroyer = Utilities.F.GetOrAddComponent<OutOfRangeDestroyer>(inst.gameObject);
destroyer.timeUntilDestroyed = 5;
destroyer.range = 300;
return inst;
}

View file

@ -0,0 +1,15 @@
using UnityEngine;
namespace SanAndreasUnity.Behaviours.World
{
public class AddFocusPointWhenLoaderFinishes : MonoBehaviour
{
public FocusPoint.Parameters parameters = FocusPoint.Parameters.Default;
private void OnLoaderFinished()
{
FocusPoint.Create(this.gameObject, this.parameters);
}
}
}

View file

@ -1,10 +1,10 @@
fileFormatVersion: 2
guid: b41c44cfa34ff5d4595e41acf7913a84
guid: f261590297a9b11459cc24b5b949c608
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 15200
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:

View file

@ -1,60 +1,116 @@
using SanAndreasUnity.Behaviours.Vehicles;
using System;
using SanAndreasUnity.Behaviours.Vehicles;
using SanAndreasUnity.Importing.Items;
using SanAndreasUnity.Importing.Items.Placements;
using SanAndreasUnity.Utilities;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using UnityEngine;
//using Facepunch.Networking;
using Profiler = UnityEngine.Profiling.Profiler;
namespace SanAndreasUnity.Behaviours.World
{
public class Cell : MonoBehaviour
{
private Stopwatch _timer;
private List<Division> _leaves;
private Dictionary<Instance, StaticGeometry> m_insts;
private Dictionary<Instance, StaticGeometry> m_insts;
public int NumStaticGeometries => m_insts.Count;
private MapObject[] m_cars;
private List<EntranceExitMapObject> m_enexes;
public Division RootDivision { get; private set; }
private List<int> CellIds = Enumerable.Range(0, 19).ToList();
public bool HasExterior => this.CellIds.Contains(0);
public bool HasMainExterior => this.CellIds.Contains(0);
public Camera PreviewCamera;
public List<Transform> focusPoints = new List<Transform> ();
public struct FocusPointInfo
{
public long id;
public Transform transform;
public float timeToKeepRevealingAfterRemoved;
public float timeWhenRemoved;
public bool hasRevealRadius;
}
private List<FocusPointInfo> _focusPoints = new List<FocusPointInfo>();
public IReadOnlyList<FocusPointInfo> FocusPoints => _focusPoints;
private List<FocusPointInfo> _focusPointsToRemoveAfterTimeout = new List<FocusPointInfo>();
private struct AreaWithDistance
{
public WorldSystem<MapObject>.Area area;
public float distance;
}
private class AreaWithDistanceComparer : IComparer<AreaWithDistance>
{
public int Compare(AreaWithDistance a, AreaWithDistance b)
{
if (a.distance < b.distance)
return -1;
if (a.distance > b.distance)
return 1;
// solve potential problem that 2 areas have same distance
// - these areas may not be equal, they may belong to different world systems and have same index,
// therefore their distances will be equal
// - by comparing their ids, we are sure that comparer is always deterministic
return a.area.Id.CompareTo(b.area.Id);
}
}
private readonly SortedSet<AreaWithDistance> _areasToUpdate = new SortedSet<AreaWithDistance>(new AreaWithDistanceComparer());
private readonly AreaWithDistance[] _bufferOfAreasToUpdate = new AreaWithDistance[64];
private int _indexOfBufferOfAreasToUpdate = 0;
private int _numElementsInBufferOfAreasToUpdate = 0;
public ushort maxTimeToUpdatePerFrameMs = 10;
private System.Diagnostics.Stopwatch _updateTimeLimitStopwatch = new System.Diagnostics.Stopwatch();
public Water Water;
public static Cell Instance { get ; private set; }
// Statistics
public float divisionRefreshDistanceDelta = 20;
private int totalNumObjects = 0;
private int numLeavesLoadedThisFrame = 0;
private int numObjectsLoadedThisFrame = 0;
private float[] measuredTimes = new float[3];
private int numDivisionsUpdatedLoadOrder = 0;
private int numMapObjectsUpdatedLoadOrder = 0;
private Division containingDivision = null;
private float _maxDrawDistance = 0;
public float MaxDrawDistance
{
get => _maxDrawDistance;
set
{
if (_maxDrawDistance == value)
return;
public float divisionLoadOrderDistanceFactor = 16;
_maxDrawDistance = value;
public float divisionRefreshDistanceDelta = 20;
this.OnMaxDrawDistanceChanged();
}
}
[Range(0.1f, 3f)]
public float divisionsUpdateInterval = 0.3f;
public float[] drawDistancesPerLayers = new float[] { 301, 801, 1501 };
public float maxDrawDistance = 500;
private WorldSystemWithDistanceLevels<MapObject> _worldSystem;
public WorldSystemWithDistanceLevels<MapObject> WorldSystem => _worldSystem;
public uint yWorldSystemSize = 25000;
public ushort yWorldSystemNumAreas = 3;
public ushort[] xzWorldSystemNumAreasPerDrawDistanceLevel = { 100, 100, 100 };
public float interiorHeightOffset = 5000f;
public float fadeRate = 2f;
public bool loadParkedVehicles = true;
public GameObject mapObjectActivatorPrefab;
public GameObject staticGeometryPrefab;
public GameObject enexPrefab;
public GameObject lightSourcePrefab;
@ -74,23 +130,9 @@ namespace SanAndreasUnity.Behaviours.World
}
private void Start()
{
//InvokeRepeating("UpdateDivisions", 0f, 0.1f);
StartCoroutine( this.UpdateDivisionsCoroutine () );
}
internal void CreateStaticGeometry ()
internal void CreateStaticGeometry ()
{
if (RootDivision == null)
{
RootDivision = Division.Create(transform);
RootDivision.SetBounds(
new Vector2(-3000f, -3000f),
new Vector2(+3000f, +3000f));
}
var placements = Item.GetPlacements<Instance>(CellIds.ToArray());
m_insts = new Dictionary<Instance,StaticGeometry> (48 * 1024);
@ -101,14 +143,25 @@ namespace SanAndreasUnity.Behaviours.World
UnityEngine.Debug.Log("Num static geometries " + m_insts.Count);
totalNumObjects = m_insts.Count;
uint worldSize = 6000;
_worldSystem = new WorldSystemWithDistanceLevels<MapObject>(
this.drawDistancesPerLayers,
this.xzWorldSystemNumAreasPerDrawDistanceLevel.Select(_ => new WorldSystemParams { worldSize = worldSize, numAreasPerAxis = _ }).ToArray(),
Enumerable.Range(0, this.drawDistancesPerLayers.Length).Select(_ => new WorldSystemParams { worldSize = this.yWorldSystemSize, numAreasPerAxis = this.yWorldSystemNumAreas }).ToArray(),
this.OnAreaChangedVisibility);
}
internal void InitStaticGeometry ()
{
foreach (var inst in m_insts)
{
inst.Value.Initialize(inst.Key, m_insts);
var staticGeometry = inst.Value;
staticGeometry.Initialize(inst.Key, m_insts);
_worldSystem.AddObjectToArea(
staticGeometry.transform.position,
staticGeometry.ObjectDefinition?.DrawDist ?? 0,
staticGeometry);
}
}
@ -117,7 +170,7 @@ namespace SanAndreasUnity.Behaviours.World
if (loadParkedVehicles)
{
var parkedVehicles = Item.GetPlacements<ParkedVehicle> (CellIds.ToArray ());
m_cars = parkedVehicles.Select (x => VehicleSpawner.Create (x))
m_cars = parkedVehicles.Select (x => VehicleSpawnMapObject.Create (x))
.Cast<MapObject> ()
.ToArray ();
@ -130,25 +183,13 @@ namespace SanAndreasUnity.Behaviours.World
m_enexes = new List<EntranceExitMapObject>(256);
foreach(var enex in Item.Enexes.Where(enex => this.CellIds.Contains(enex.TargetInterior)))
{
m_enexes.Add(EntranceExitMapObject.Create(enex));
var enexComponent = EntranceExitMapObject.Create(enex);
m_enexes.Add(enexComponent);
_worldSystem.AddObjectToArea(enexComponent.transform.position, 100f, enexComponent);
}
}
internal void AddMapObjectsToDivisions ()
{
var enumerable = m_insts.Values.Cast<MapObject> ();
if (m_cars != null)
enumerable = enumerable.Concat (m_cars);
if (m_enexes != null)
enumerable = enumerable.Concat(m_enexes);
RootDivision.AddRange (enumerable);
}
internal void LoadWater ()
internal void LoadWater ()
{
if (Water != null)
{
@ -161,29 +202,116 @@ namespace SanAndreasUnity.Behaviours.World
// set layer recursively for all game objects
// this.gameObject.SetLayerRecursive( this.gameObject.layer );
_timer = new Stopwatch();
_leaves = RootDivision.ToList();
}
private void OnAreaChangedVisibility(WorldSystem<MapObject>.Area area, bool visible)
{
if (null == area.ObjectsInside || area.ObjectsInside.Count == 0)
return;
Profiler.BeginSample("OnAreaChangedVisibility");
Vector3 areaCenter = area.WorldSystem.GetAreaCenter(area);
float minSqrDistance = area.FocusPointsThatSeeMe?.Min(f => (areaCenter - f.Position).sqrMagnitude) ?? float.PositiveInfinity;
_areasToUpdate.Add(new AreaWithDistance
{
area = area,
distance = minSqrDistance,
});
Profiler.EndSample();
}
public void RegisterFocusPoint(Transform tr, FocusPoint.Parameters parameters)
{
if (!_focusPoints.Exists(f => f.transform == tr))
{
float revealRadius = parameters.hasRevealRadius ? parameters.revealRadius : this.MaxDrawDistance;
long registeredFocusPointId = _worldSystem.RegisterFocusPoint(revealRadius, tr.position);
_focusPoints.Add(new FocusPointInfo
{
id = registeredFocusPointId,
transform = tr,
timeToKeepRevealingAfterRemoved = parameters.timeToKeepRevealingAfterRemoved,
hasRevealRadius = parameters.hasRevealRadius,
});
}
}
public void UnRegisterFocusPoint(Transform tr)
{
int index = _focusPoints.FindIndex(f => f.transform == tr);
if (index < 0)
return;
// maybe we could just set transform to null, so it gets removed during next update ?
var focusPoint = _focusPoints[index];
if (focusPoint.timeToKeepRevealingAfterRemoved > 0)
{
focusPoint.timeWhenRemoved = Time.time;
_focusPointsToRemoveAfterTimeout.Add(focusPoint);
_focusPoints.RemoveAt(index);
return;
}
_worldSystem.UnRegisterFocusPoint(focusPoint.id);
_focusPoints.RemoveAt(index);
}
private void OnMaxDrawDistanceChanged()
{
if (null == _worldSystem)
return;
for (int i = 0; i < _focusPoints.Count; i++)
{
var focusPoint = _focusPoints[i];
if (!focusPoint.hasRevealRadius)
{
_worldSystem.FocusPointChangedRadius(focusPoint.id, this.MaxDrawDistance);
}
}
}
public IEnumerable<EntranceExit> GetEnexesFromLoadedInteriors()
{
int[] loadedInteriors = this.CellIds.Where(id => id != 0 && id != 13).ToArray();
int[] loadedInteriors = this.CellIds.Where(id => !IsExteriorLevel(id)).ToArray();
foreach(var enex in Importing.Items.Item.Enexes.Where(enex => loadedInteriors.Contains(enex.TargetInterior)))
{
yield return enex;
}
}
public static TransformDataStruct GetEnexExitTransform(EntranceExit enex)
public TransformDataStruct GetEnexExitTransform(EntranceExit enex)
{
return new TransformDataStruct(enex.ExitPos + Vector3.up * 0.2f, Quaternion.Euler(0f, enex.ExitAngle, 0f));
return new TransformDataStruct(
this.GetPositionBasedOnInteriorLevel(enex.ExitPos + Vector3.up * 0.2f, enex.TargetInterior),
Quaternion.Euler(0f, enex.ExitAngle, 0f));
}
public static TransformDataStruct GetEnexEntranceTransform(EntranceExit enex)
public TransformDataStruct GetEnexEntranceTransform(EntranceExit enex)
{
return new TransformDataStruct(enex.EntrancePos + Vector3.up * 0.2f, Quaternion.Euler(0f, enex.EntranceAngle, 0f));
return new TransformDataStruct(
this.GetPositionBasedOnInteriorLevel(enex.EntrancePos + Vector3.up * 0.2f, enex.TargetInterior),
Quaternion.Euler(0f, enex.EntranceAngle, 0f));
}
public static bool IsExteriorLevel(int interiorLevel)
{
return interiorLevel == 0 || interiorLevel == 13;
}
public Vector3 GetPositionBasedOnInteriorLevel(Vector3 originalPos, int interiorLevel)
{
if (!IsExteriorLevel(interiorLevel))
originalPos.y += this.interiorHeightOffset;
return originalPos;
}
@ -193,166 +321,135 @@ namespace SanAndreasUnity.Behaviours.World
if (!Loader.HasLoaded)
return;
//this.Setup ();
_updateTimeLimitStopwatch.Restart();
if (null == _leaves)
return;
float timeNow = Time.time;
_timer.Reset();
_timer.Start();
numLeavesLoadedThisFrame = 0;
numObjectsLoadedThisFrame = 0;
UnityEngine.Profiling.Profiler.BeginSample("Update focus points");
this._focusPoints.RemoveAll(f =>
{
if (null == f.transform)
{
if (f.timeToKeepRevealingAfterRemoved > 0f)
{
f.timeWhenRemoved = timeNow;
_focusPointsToRemoveAfterTimeout.Add(f);
return true;
}
this.focusPoints.RemoveDeadObjects();
if (this.focusPoints.Count > 0)
UnityEngine.Profiling.Profiler.BeginSample("WorldSystem.UnRegisterFocusPoint()");
_worldSystem.UnRegisterFocusPoint(f.id);
UnityEngine.Profiling.Profiler.EndSample();
return true;
}
_worldSystem.FocusPointChangedPosition(f.id, f.transform.position);
return false;
});
UnityEngine.Profiling.Profiler.EndSample();
bool hasElementToRemove = false;
_focusPointsToRemoveAfterTimeout.ForEach(_ =>
{
if (timeNow - _.timeWhenRemoved > _.timeToKeepRevealingAfterRemoved)
{
hasElementToRemove = true;
UnityEngine.Profiling.Profiler.BeginSample("WorldSystem.UnRegisterFocusPoint()");
_worldSystem.UnRegisterFocusPoint(_.id);
UnityEngine.Profiling.Profiler.EndSample();
}
});
if (hasElementToRemove)
_focusPointsToRemoveAfterTimeout.RemoveAll(_ => timeNow - _.timeWhenRemoved > _.timeToKeepRevealingAfterRemoved);
if (this._focusPoints.Count > 0)
{
// only update divisions loading if there are focus points - because otherwise,
// load order of divisions is not updated
this.UpdateDivisionsLoading();
}
measuredTimes[2] = (float)_timer.Elapsed.TotalMilliseconds;
UnityEngine.Profiling.Profiler.BeginSample("WorldSystem.Update()");
_worldSystem.Update();
UnityEngine.Profiling.Profiler.EndSample();
}
Profiler.BeginSample("UpdateAreasLoop");
void UpdateDivisionsLoading()
{
foreach (var div in _leaves)
while (true)
{
if (float.IsPositiveInfinity(div.LoadOrder))
break;
if (_areasToUpdate.Count == 0 && _numElementsInBufferOfAreasToUpdate == 0)
break;
numObjectsLoadedThisFrame += div.LoadWhile(() => _timer.Elapsed.TotalSeconds < 1d / 60d);
if (_updateTimeLimitStopwatch.ElapsedMilliseconds >= this.maxTimeToUpdatePerFrameMs)
break;
if (_timer.Elapsed.TotalSeconds >= 1d / 60d)
{
// break;
}
else
{
numLeavesLoadedThisFrame++;
}
}
}
/*_areasToUpdate.CopyTo();
_areasToUpdate.Clear(); // very good
_areasToUpdate.ExceptWith(); // seems good
_areasToUpdate.UnionWith(); // catastrophic*/
System.Collections.IEnumerator UpdateDivisionsCoroutine ()
{
if (_numElementsInBufferOfAreasToUpdate == 0)
{
Profiler.BeginSample("TakeFromSortedSet");
while (true)
{
// wait 100 ms
float timePassed = 0;
while (timePassed < this.divisionsUpdateInterval)
{
yield return null;
timePassed += Time.unscaledDeltaTime;
}
// we processed all areas from buffer
// take some more from SortedSet
int numToCopy = Mathf.Min(_bufferOfAreasToUpdate.Length, _areasToUpdate.Count);
_areasToUpdate.CopyTo(_bufferOfAreasToUpdate, 0, numToCopy);
F.RunExceptionSafe (() => this.UpdateDivisions ());
for (int i = 0; i < numToCopy; i++)
{
if (!_areasToUpdate.Remove(_bufferOfAreasToUpdate[i]))
throw new Exception($"Failed to remove area {_bufferOfAreasToUpdate[i].area.Id} from SortedSet");
}
}
//_areasToUpdate.ExceptWith(new System.ArraySegment<AreaWithDistance>(_bufferOfAreasToUpdate, 0, numToCopy));
}
_indexOfBufferOfAreasToUpdate = 0;
_numElementsInBufferOfAreasToUpdate = numToCopy;
private void UpdateDivisions()
{
if (!Loader.HasLoaded)
return;
if (_leaves == null) return;
Profiler.EndSample();
}
this.focusPoints.RemoveAll (t => null == t);
// process 1 area from buffer
if (this.focusPoints.Count < 1)
return;
var areaWithDistance = _bufferOfAreasToUpdate[_indexOfBufferOfAreasToUpdate];
_indexOfBufferOfAreasToUpdate++;
_numElementsInBufferOfAreasToUpdate--;
numDivisionsUpdatedLoadOrder = 0;
numMapObjectsUpdatedLoadOrder = 0;
containingDivision = null;
this.UpdateArea(areaWithDistance);
_timer.Reset();
_timer.Start();
List<Vector3> positions = this.focusPoints.Select (f => f.position).ToList ();
bool toLoad = false; // _leaves.Aggregate(false, (current, leaf) => current | leaf.RefreshLoadOrder(pos));
UnityEngine.Profiling.Profiler.BeginSample ("Update divisions", this);
foreach (Division leaf in _leaves)
{
Vector3 pos = leaf.GetClosestPosition (positions);
int count = 0;
toLoad |= leaf.RefreshLoadOrder(pos, out count);
if (count > 0)
{
numDivisionsUpdatedLoadOrder++;
numMapObjectsUpdatedLoadOrder += count;
}
if (null == containingDivision && leaf.Contains(pos))
{
containingDivision = leaf;
}
}
UnityEngine.Profiling.Profiler.EndSample ();
Profiler.EndSample();
measuredTimes[0] = (float)_timer.Elapsed.TotalMilliseconds;
if (!toLoad) return;
_timer.Reset();
_timer.Start();
UnityEngine.Profiling.Profiler.BeginSample ("Sort leaves", this);
_leaves.Sort();
UnityEngine.Profiling.Profiler.EndSample ();
measuredTimes[1] = (float)_timer.Elapsed.TotalMilliseconds;
}
/*
private static Rect windowRect = new Rect(10, 10, 250, 330);
private const int windowID = 0;
private void OnGUI()
void UpdateArea(AreaWithDistance areaWithDistance)
{
if (!Loader.HasLoaded)
return;
var area = areaWithDistance.area;
bool visible = area.WasVisibleInLastUpdate;
if (!PlayerController._showMenu)
return;
for (int i = 0; i < area.ObjectsInside.Count; i++)
{
var obj = area.ObjectsInside[i];
windowRect = GUILayout.Window(windowID, windowRect, showWindow, "World statistics");
}
*/
if (visible == obj.IsVisibleInMapSystem)
continue;
public void showWindow(int windowID)
{
GUILayout.Label("draw distance " + this.maxDrawDistance);
GUILayout.Label("num focus points " + this.focusPoints.Count);
GUILayout.Label("total num divisions " + (null == _leaves ? 0 : _leaves.Count));
GUILayout.Label("total num objects " + totalNumObjects);
GUILayout.Label("geometry parts loaded " + SanAndreasUnity.Importing.Conversion.Geometry.NumGeometryPartsLoaded);
GUILayout.Label("num TOBJ objects " + StaticGeometry.TimedObjects.Count);
GUILayout.Label("num active objects with lights " + StaticGeometry.ActiveObjectsWithLights.Count);
GUILayout.Label("num objects in current division " + (containingDivision != null ? containingDivision.NumObjects : 0));
GUILayout.Label("num divisions updated " + numDivisionsUpdatedLoadOrder);
GUILayout.Label("num objects updated " + numMapObjectsUpdatedLoadOrder);
GUILayout.Label("num divisions loading this frame " + numLeavesLoadedThisFrame);
GUILayout.Label("num objects loading this frame " + numObjectsLoadedThisFrame);
F.RunExceptionSafe(() =>
{
if (visible)
{
obj.Show(areaWithDistance.distance);
}
else
obj.UnShow();
});
}
GUILayout.Space(10);
string[] timeNames = new string[] { "refresh load order ", "sort ", "load / update display " };
int i = 0;
foreach (float time in measuredTimes)
{
GUILayout.Label(timeNames[i] + Mathf.RoundToInt(time));
i++;
}
GUI.DragWindow();
}
}
}

View file

@ -1,372 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using SanAndreasUnity.Utilities;
namespace SanAndreasUnity.Behaviours.World
{
public class Division : MonoBehaviour, IEnumerable<Division>, IComparable<Division>
{
private static readonly Comparison<MapObject> _sHorzSort =
(a, b) => Math.Sign(a.CellPos.x - b.CellPos.x);
private static readonly Comparison<MapObject> _sVertSort =
(a, b) => Math.Sign(a.CellPos.y - b.CellPos.y);
public static Division Create(Transform parent)
{
var obj = new GameObject();
var split = obj.AddComponent<Division>();
obj.transform.SetParent(parent);
return split;
}
private const int LeafObjectLimit = 127;
private Division _childA;
private Division _childB;
private List<MapObject> _objects;
public int NumObjects { get { return _objects != null ? _objects.Count : 0; } }
public int NumObjectsIncludingChildren
{
get
{
int count = this.NumObjects;
if (_childA != null)
count += _childA.NumObjectsIncludingChildren;
if (_childB != null)
count += _childB.NumObjectsIncludingChildren;
return count;
}
}
private bool _isVertSplit;
private float _splitVal;
private Vector3 _lastRefreshPos;
public Vector2 Min { get; private set; }
public Vector2 Max { get; private set; }
private Bounds _bounds;
public bool IsSubdivided { get { return _objects == null; } }
internal float LoadOrder { get; private set; }
public void SetBounds(Vector2 min, Vector2 max)
{
Min = min;
Max = max;
_bounds = new Bounds(((this.Min + this.Max) * 0.5f).ToVector3XZ(), (this.Max - this.Min).ToVector3XZ().WithY(10000f));
var mid = (Max + Min) * .5f;
if (float.IsNaN(mid.x) || float.IsInfinity(mid.x))
{
mid.x = 0f;
}
if (float.IsNaN(mid.y) || float.IsInfinity(mid.y))
{
mid.y = 0f;
}
transform.position = new Vector3(mid.x, 0f, mid.y);
name = String.Format("Split {0}, {1}", min, max);
_objects = _objects ?? new List<MapObject>();
}
private void Subdivide()
{
if (IsSubdivided)
{
throw new InvalidOperationException("Already subdivided");
}
if (_objects.Count == 0)
{
throw new InvalidOperationException("Cannot subdivide an empty leaf");
}
var min = Max;
var max = Min;
foreach (var obj in _objects)
{
var pos = obj.CellPos;
min.x = Mathf.Min(pos.x, min.x);
min.y = Mathf.Min(pos.y, min.y);
max.x = Mathf.Max(pos.x, max.x);
max.y = Mathf.Max(pos.y, max.y);
}
_isVertSplit = max.x - min.x >= max.y - min.y;
_objects.Sort(_isVertSplit ? _sHorzSort : _sVertSort);
_childA = Create(transform);
_childB = Create(transform);
var mid = _objects.Count / 2;
var median = (_objects[mid - 1].CellPos + _objects[mid].CellPos) * .5f;
if (_isVertSplit)
{
_splitVal = median.x;
_childA.SetBounds(Min, new Vector2(_splitVal, Max.y));
_childB.SetBounds(new Vector2(_splitVal, Min.y), Max);
}
else
{
_splitVal = median.y;
_childA.SetBounds(Min, new Vector2(Max.x, _splitVal));
_childB.SetBounds(new Vector2(Min.x, _splitVal), Max);
}
_childA._objects = _objects;
_childB._objects = new List<MapObject>();
_childB._objects.AddRange(_childA._objects.Skip(mid));
_childA._objects.RemoveRange(mid, _objects.Count - mid);
_objects = null;
}
private void AddInternal(MapObject obj)
{
if (IsSubdivided)
{
var comp = _isVertSplit ? obj.CellPos.x : obj.CellPos.y;
(comp < _splitVal ? _childA : _childB).AddInternal(obj);
return;
}
_objects.Add(obj);
if (_objects.Count > LeafObjectLimit) Subdivide();
}
public void Add(MapObject obj)
{
AddInternal(obj);
UpdateParents();
}
public void AddRange(IEnumerable<MapObject> objs)
{
foreach (var obj in objs.OrderBy(x => x.RandomInt))
{
AddInternal(obj);
}
UpdateParents();
}
private void UpdateParents()
{
if (IsSubdivided)
{
_childA.UpdateParents();
_childB.UpdateParents();
}
else
{
if (_objects.Count == 0) return;
var sum = _objects.Aggregate(new Vector2(), (s, x) => s + x.CellPos);
transform.position = new Vector3(sum.x / _objects.Count, 0f, sum.y / _objects.Count);
foreach (var obj in _objects)
{
obj.transform.SetParent(transform, true);
}
}
}
public bool Contains(MapObject obj)
{
return Contains(obj.CellPos);
}
public bool Contains(Vector3 pos)
{
return pos.x >= Min.x && pos.z >= Min.y && pos.x < Max.x && pos.z < Max.y;
}
public bool Contains(Vector2 pos)
{
return pos.x >= Min.x && pos.y >= Min.y && pos.x < Max.x && pos.y < Max.y;
}
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.green;
Gizmos.DrawWireCube(_bounds.center, _bounds.size.WithY(100f));
F.HandlesDrawText(_bounds.center,
string.Format("total objects {0}, my objects {1}, load order {2}", this.NumObjectsIncludingChildren, this.NumObjects, this.LoadOrder),
Color.green);
/*
if (!IsSubdivided) return;
Gizmos.color = Color.green;
var min = new Vector2(Math.Max(Min.x, -8192f), Math.Max(Min.y, -8192f));
var max = new Vector2(Math.Min(Max.x, +8192f), Math.Min(Max.y, +8192f));
if (_isVertSplit)
{
Gizmos.DrawLine(new Vector3(_splitVal, 0f, min.y), new Vector3(_splitVal, 0f, max.y));
}
else
{
Gizmos.DrawLine(new Vector3(min.x, 0f, _splitVal), new Vector3(max.x, 0f, _splitVal));
}
*/
}
public float GetDistance(Vector3 pos)
{
return Mathf.Sqrt(GetDistanceSquared(pos));
}
public float GetDistanceSquared(Vector3 pos)
{
pos.y = 0f; // only count X and Z axis
// get the closest point on bounds
// if position is inside bounds, the resulting distance will be 0
Vector3 closestPos = _bounds.ClosestPoint(pos);
return Vector3.SqrMagnitude(pos - closestPos);
}
public Vector3 GetClosestPosition (List<Vector3> positions)
{
Vector3 closestPos = positions [0];
float smallestDist2 = float.MaxValue;
Vector2 center = (this.Min + this.Max) * 0.5f;
for (int i = 0; i < positions.Count; i++)
{
float dist2 = Vector2.SqrMagnitude (positions [i].ToVec2WithXAndZ() - center);
if (dist2 <= smallestDist2)
{
smallestDist2 = dist2;
closestPos = positions [i];
}
}
return closestPos;
}
public bool RefreshLoadOrder(Vector3 from, out int numMapObjectsUpdatedLoadOrder)
{
UnityEngine.Profiling.Profiler.BeginSample ("Division.RefreshLoadOrder", this);
var toLoad = false;
numMapObjectsUpdatedLoadOrder = 0;
if (GetDistanceSquared(from) <= Cell.Instance.maxDrawDistance * Cell.Instance.maxDrawDistance)
{
float divisionRefreshDistanceDeltaSquared = Cell.Instance.divisionRefreshDistanceDelta * Cell.Instance.divisionRefreshDistanceDelta;
// float factor = Cell.Instance.divisionLoadOrderDistanceFactor; //16;
// if (Vector3.SqrMagnitude(from - _lastRefreshPos) > GetDistanceSquared(from) / (factor*factor)) {
if (Vector3.SqrMagnitude(from - _lastRefreshPos) > divisionRefreshDistanceDeltaSquared)
{
_lastRefreshPos = from;
foreach (var obj in _objects)
{
bool b = obj.RefreshLoadOrder(from);
if (b)
{
toLoad = true;
numMapObjectsUpdatedLoadOrder++;
}
}
}
else
{
toLoad = _objects.Any(x => !float.IsPositiveInfinity(x.LoadOrder));
}
}
if (toLoad)
{
_objects.Sort(); // THIS MAY BE PERFORMANCE DROP
LoadOrder = _objects[0].LoadOrder;
}
else
{
LoadOrder = float.PositiveInfinity;
}
UnityEngine.Profiling.Profiler.EndSample ();
return toLoad;
}
public int LoadWhile(Func<bool> predicate)
{
UnityEngine.Profiling.Profiler.BeginSample ("LoadWhile", this);
int numLoaded = 0;
foreach (var toLoad in _objects)
{
if (float.IsPositiveInfinity(toLoad.LoadOrder))
break;
if (toLoad.HasLoaded)
{
// this object is loaded, just show it
toLoad.Show();
}
else
{
// this object is still not loaded from disk
// check if we should load it
if (predicate())
{
toLoad.Show();
numLoaded++;
}
}
}
UnityEngine.Profiling.Profiler.EndSample ();
// return predicate();
// return false ;
return numLoaded;
}
public IEnumerator<Division> GetEnumerator()
{
if (IsSubdivided)
{
return _childA.Concat(_childB).GetEnumerator();
}
return new[] { this }.AsEnumerable().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public int CompareTo(Division other)
{
return LoadOrder > other.LoadOrder ? 1 : LoadOrder == other.LoadOrder ? 0 : -1;
}
}
}

View file

@ -2,14 +2,15 @@
using SanAndreasUnity.Importing.Items.Placements;
using SanAndreasUnity.Utilities;
using System.Collections.Generic;
using System.Linq;
namespace SanAndreasUnity.Behaviours.World
{
public class EntranceExitMapObject : MapObject
{
static List<EntranceExitMapObject> s_allObjects = new List<EntranceExitMapObject>();
public static IEnumerable<EntranceExitMapObject> AllObjects => s_allObjects;
static List<EntranceExitMapObject> _sAllActiveObjects = new List<EntranceExitMapObject>();
public static IReadOnlyList<EntranceExitMapObject> AllActiveObjects => _sAllActiveObjects;
Coroutine m_animateArrowCoroutine;
@ -22,7 +23,7 @@ namespace SanAndreasUnity.Behaviours.World
public static EntranceExitMapObject Create(EntranceExit info)
{
var obj = Object.Instantiate(Cell.Instance.enexPrefab).GetComponent<EntranceExitMapObject>();
var obj = Create<EntranceExitMapObject>(Cell.Instance.enexPrefab);
obj.Initialize(info);
return obj;
}
@ -37,7 +38,11 @@ namespace SanAndreasUnity.Behaviours.World
float height = 2f;
Initialize(info.EntrancePos + Vector3.up * height * 0.5f, Quaternion.identity);
this.Initialize(
Cell.Instance.GetPositionBasedOnInteriorLevel(
info.EntrancePos + Vector3.up * height * 0.5f,
info.TargetInterior),
Quaternion.identity);
gameObject.SetActive(false);
gameObject.isStatic = true;
@ -52,17 +57,19 @@ namespace SanAndreasUnity.Behaviours.World
rb.mass = 0f;
rb.isKinematic = true;
this.SetDrawDistance(100f);
}
void OnEnable()
{
s_allObjects.Add(this);
_sAllActiveObjects.Add(this);
m_animateArrowCoroutine = this.StartCoroutine(this.AnimateArrow());
}
void OnDisable()
{
s_allObjects.Remove(this);
_sAllActiveObjects.Remove(this);
if (m_animateArrowCoroutine != null)
this.StopCoroutine(m_animateArrowCoroutine);
@ -75,19 +82,6 @@ namespace SanAndreasUnity.Behaviours.World
Utilities.F.HandlesDrawText(this.transform.position, this.name, Color.yellow);
}
protected override float OnRefreshLoadOrder(Vector3 from)
{
float dist = Vector3.Distance(from, this.transform.position);
if (dist > 100f)
{
this.gameObject.SetActive(false);
return float.PositiveInfinity;
}
return dist * dist;
}
protected override void OnLoad()
{
//Debug.LogFormat("OnLoad() - {0}", this.gameObject.name);
@ -150,6 +144,32 @@ namespace SanAndreasUnity.Behaviours.World
}
public EntranceExit FindMatchingEnex()
{
EntranceExit firstMatchingWithDifferentInterior = null;
EntranceExit firstMatchingWithSameInterior = null;
foreach (var enex in Importing.Items.Item.Enexes)
{
if (firstMatchingWithDifferentInterior != null && firstMatchingWithSameInterior != null) // both are found
continue;
if (enex.Name != this.Info.Name)
continue;
if (enex == this.Info)
continue;
if (null == firstMatchingWithDifferentInterior && enex.TargetInterior != this.Info.TargetInterior)
firstMatchingWithDifferentInterior = enex;
if (null == firstMatchingWithSameInterior && enex.TargetInterior == this.Info.TargetInterior)
firstMatchingWithSameInterior = enex;
}
return firstMatchingWithDifferentInterior ?? firstMatchingWithSameInterior;
}
}
}

View file

@ -0,0 +1,48 @@
using System;
using UnityEngine;
namespace SanAndreasUnity.Behaviours.World
{
[DisallowMultipleComponent]
public class FocusPoint : MonoBehaviour
{
[Serializable]
public struct Parameters
{
public bool hasRevealRadius;
public float revealRadius;
public float timeToKeepRevealingAfterRemoved;
public Parameters(bool hasRevealRadius, float revealRadius, float timeToKeepRevealingAfterRemoved)
{
this.hasRevealRadius = hasRevealRadius;
this.revealRadius = revealRadius;
this.timeToKeepRevealingAfterRemoved = timeToKeepRevealingAfterRemoved;
}
public static Parameters Default => new Parameters(true, 150f, 3f);
}
public Parameters parameters = Parameters.Default;
public static FocusPoint Create(GameObject targetGameObject, Parameters parameters)
{
var focusPoint = targetGameObject.AddComponent<FocusPoint>();
focusPoint.parameters = parameters;
return focusPoint;
}
private void Start()
{
if (Cell.Instance != null)
Cell.Instance.RegisterFocusPoint(this.transform, this.parameters);
}
private void OnDisable()
{
if (Cell.Instance != null)
Cell.Instance.UnRegisterFocusPoint(this.transform);
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8643b1a14caafa547beb3c2e2780085f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -8,6 +8,7 @@ using System.Linq;
using SanAndreasUnity.Importing.RenderWareStream;
using SanAndreasUnity.Utilities;
using UnityEngine;
using UnityEngine.Rendering;
using Geometry = SanAndreasUnity.Importing.Conversion.Geometry;
using Profiler = UnityEngine.Profiling.Profiler;
@ -23,39 +24,25 @@ namespace SanAndreasUnity.Behaviours.World
DayTimeManager.Singleton.onHourChanged += OnHourChanged;
}
return new GameObject().AddComponent<StaticGeometry>();
return Create<StaticGeometry>(Cell.Instance.staticGeometryPrefab);
}
private static List<StaticGeometry> s_timedObjects = new List<StaticGeometry>();
public static IReadOnlyList<StaticGeometry> TimedObjects => s_timedObjects;
protected Instance Instance { get; private set; }
public Instance Instance { get; private set; }
public ISimpleObjectDefinition ObjectDefinition { get; private set; }
private bool _canLoad;
private bool _isGeometryLoaded = false;
private bool _isVisible;
private bool _isFading;
private bool _isFading;
public bool IsVisible
{
get { return _isVisible; }
private set
{
if (_isVisible == value) return;
_isVisible = value;
gameObject.SetActive(true);
StartCoroutine(Fade());
if (value && LodChild != null)
{
LodChild.Hide();
}
}
}
public bool ShouldBeVisibleNow =>
this.IsVisibleInMapSystem
&& this.IsVisibleBasedOnCurrentDayTime
&& _isGeometryLoaded
&& (LodParent == null || !LodParent.ShouldBeVisibleNow);
public StaticGeometry LodParent { get; private set; }
public StaticGeometry LodChild { get; private set; }
@ -77,6 +64,8 @@ namespace SanAndreasUnity.Behaviours.World
private bool m_hasTrafficLights = false;
private int m_activeTrafficLightIndex = -1;
public int NumLightSources => m_lightSources?.Length ?? 0;
private void OnEnable()
{
@ -107,7 +96,9 @@ namespace SanAndreasUnity.Behaviours.World
s_timedObjects.Add(this);
}
Initialize(inst.Position, inst.Rotation);
this.Initialize(
Cell.Instance.GetPositionBasedOnInteriorLevel(inst.Position, inst.InteriorLevel),
inst.Rotation);
_canLoad = ObjectDefinition != null;
@ -122,55 +113,12 @@ namespace SanAndreasUnity.Behaviours.World
}
}
_isVisible = false;
this.SetDrawDistance(ObjectDefinition?.DrawDist ?? 0);
gameObject.SetActive(false);
gameObject.isStatic = true;
}
public bool ShouldBeVisible(Vector3 from)
{
if (!_canLoad) return false;
var obj = ObjectDefinition;
// if (obj.HasFlag (ObjectFlag.DisableDrawDist))
// return true;
// var dist = Vector3.Distance(from, transform.position);
var distSquared = Vector3.SqrMagnitude(from - transform.position);
if (distSquared > Cell.Instance.maxDrawDistance * Cell.Instance.maxDrawDistance)
return false;
if (distSquared > obj.DrawDist * obj.DrawDist)
return false;
if (!HasLoaded || LodParent == null || !LodParent.IsVisible)
return true;
if (!LodParent.ShouldBeVisible(from))
return true;
return false;
// return (distSquared <= obj.DrawDist * obj.DrawDist || (obj.DrawDist >= 300 && distSquared < 2560*2560))
// && (!HasLoaded || LodParent == null || !LodParent.IsVisible || !LodParent.ShouldBeVisible(from));
}
protected override float OnRefreshLoadOrder(Vector3 from)
{
var visible = ShouldBeVisible(from);
if (!IsVisible)
{
return visible ? Vector3.SqrMagnitude(from - transform.position) : float.PositiveInfinity;
}
if (!visible) Hide();
return float.PositiveInfinity;
}
protected override void OnLoad()
{
@ -179,25 +127,18 @@ namespace SanAndreasUnity.Behaviours.World
Profiler.BeginSample ("StaticGeometry.OnLoad", this);
// this was previously placed after loading geometry
Flags = Enum.GetValues(typeof(ObjectFlag))
.Cast<ObjectFlag>()
.Where(x => (ObjectDefinition.Flags & x) == x)
.Select(x => x.ToString())
.ToList();
//var geoms = Geometry.Load(Instance.Object.ModelName, Instance.Object.TextureDictionaryName);
//OnGeometryLoaded (geoms);
// we could start loading collision model here
// - we can't, because we don't know the name of collision file until clump is loaded
Geometry.LoadAsync( ObjectDefinition.ModelName, new string[] {ObjectDefinition.TextureDictionaryName}, (geoms) => {
Geometry.LoadAsync( ObjectDefinition.ModelName, new string[] {ObjectDefinition.TextureDictionaryName}, this.LoadPriority, (geoms) => {
if(geoms != null)
{
// we can't load collision model asyncly, because it requires a transform to attach to
// but, we can load collision file asyncly
Importing.Collision.CollisionFile.FromNameAsync( geoms.Collisions != null ? geoms.Collisions.Name : geoms.Name, (cf) => {
Importing.Collision.CollisionFile.FromNameAsync( geoms.Collisions != null ? geoms.Collisions.Name : geoms.Name, this.LoadPriority, (cf) => {
OnGeometryLoaded (geoms);
});
}
@ -216,13 +157,18 @@ namespace SanAndreasUnity.Behaviours.World
var mf = gameObject.AddComponent<MeshFilter>();
var mr = gameObject.AddComponent<MeshRenderer>();
mr.receiveShadows = this.ShouldReceiveShadows();
mr.shadowCastingMode = this.ShouldCastShadows() ? ShadowCastingMode.On : ShadowCastingMode.Off;
mf.sharedMesh = geoms.Geometry[0].Mesh;
mr.sharedMaterials = geoms.Geometry[0].GetMaterials(ObjectDefinition.Flags, mat => mat.SetTexture(NoiseTexId, NoiseTex));
mr.sharedMaterials = geoms.Geometry[0].GetMaterials(ObjectDefinition.Flags, mat => mat.SetTexture(NoiseTexPropertyId, NoiseTex));
Profiler.EndSample ();
Profiler.BeginSample("CreateLights()", this);
if (!F.IsInHeadlessMode)
this.CreateLights(geoms);
Profiler.EndSample();
geoms.AttachCollisionModel(transform);
@ -237,6 +183,7 @@ namespace SanAndreasUnity.Behaviours.World
_isGeometryLoaded = true;
this.UpdateVisibility();
}
private void OnCollisionModelAttached ()
@ -248,11 +195,18 @@ namespace SanAndreasUnity.Behaviours.World
protected override void OnShow()
{
Profiler.BeginSample ("StaticGeometry.OnShow");
IsVisible = LodParent == null || !LodParent.IsVisible;
this.UpdateVisibility();
Profiler.EndSample ();
}
private IEnumerator Fade()
protected override void OnUnShow()
{
this.UpdateVisibility();
}
private IEnumerator FadeCoroutine()
{
if (_isFading) yield break;
@ -269,47 +223,110 @@ namespace SanAndreasUnity.Behaviours.World
yield break;
}
const float fadeRate = 2f;
float fadeRate = Cell.Instance.fadeRate;
var pb = new MaterialPropertyBlock();
// continuously change transparency until object becomes fully opaque or fully transparent
var val = IsVisible ? 0f : -1f;
float val = this.ShouldBeVisibleNow ? 0f : -1f;
for (; ; )
{
var dest = IsVisible ? 1f : 0f;
float dest = this.ShouldBeVisibleNow ? 1f : 0f;
var sign = Math.Sign(dest - val);
val += sign * fadeRate * Time.deltaTime;
if (sign == 0 || sign == 1 && val >= dest || sign == -1 && val <= dest) break;
if (sign == 0 || sign == 1 && val >= dest || sign == -1 && val <= dest)
break;
pb.SetFloat(FadeId, (float)val);
pb.SetFloat(FadePropertyId, val);
mr.SetPropertyBlock(pb);
yield return new WaitForEndOfFrame();
}
mr.SetPropertyBlock(null);
if (!IsVisible || !IsVisibleBasedOnCurrentDayTime)
{
gameObject.SetActive(false);
yield return null;
}
_isFading = false;
mr.SetPropertyBlock(null);
this.gameObject.SetActive(this.ShouldBeVisibleNow);
}
public void Hide()
private void UpdateVisibility()
{
IsVisible = false;
bool needsFading = this.NeedsFading();
this.gameObject.SetActive(needsFading || this.ShouldBeVisibleNow);
if (needsFading)
{
_isFading = false;
this.StopCoroutine(nameof(FadeCoroutine));
this.StartCoroutine(nameof(FadeCoroutine));
}
if (LodChild != null)
LodChild.UpdateVisibility();
}
private bool NeedsFading()
{
if (F.IsInHeadlessMode)
return false;
// always fade, except when parent should be visible, but he is still loading
if (LodParent == null)
return true;
if (LodParent.IsVisibleInMapSystem && !LodParent._isGeometryLoaded)
return false;
return true;
}
private bool ShouldReceiveShadows()
{
if (null == this.ObjectDefinition)
return false;
var flags = ObjectFlag.Additive // transparent
| ObjectFlag.NoZBufferWrite // transparent
| ObjectFlag.DontReceiveShadows;
if ((this.ObjectDefinition.Flags & flags) != 0)
return false;
if (LodParent != null) // LOD models should not receive shadows
return false;
return true;
}
private bool ShouldCastShadows()
{
if (null == this.ObjectDefinition)
return false;
var flags = ObjectFlag.Additive // transparent
| ObjectFlag.NoZBufferWrite; // transparent
if ((this.ObjectDefinition.Flags & flags) != 0)
return false;
// if object is LOD, only cast shadows if it has large draw distance
// - that's because his shadow may be visible from long distance
if (LodParent != null && this.ObjectDefinition.DrawDist < 1000f)
return false;
return true;
}
private static void OnHourChanged()
{
foreach (var timedObject in s_timedObjects)
{
if (timedObject.IsVisible)
if (timedObject.IsVisibleInMapSystem)
{
timedObject.gameObject.SetActive(timedObject.IsVisibleBasedOnCurrentDayTime);
}
@ -461,7 +478,7 @@ namespace SanAndreasUnity.Behaviours.World
if (null == m_lightSources)
return;
bool isDay = DayTimeManager.Singleton.CurrentTimeHours >= 6 && DayTimeManager.Singleton.CurrentTimeHours <= 20;
bool isDay = DayTimeManager.Singleton.CurrentTimeHours > 6 && DayTimeManager.Singleton.CurrentTimeHours < 18;
var flag = isDay ? TwoDEffect.Light.Flags1.AT_DAY : TwoDEffect.Light.Flags1.AT_NIGHT;
for (int i = 0; i < m_lightSources.Length; i++)

View file

@ -0,0 +1,70 @@
using UnityEngine;
using Object = UnityEngine.Object;
namespace SanAndreasUnity.Behaviours.World
{
public class WorldManager : MonoBehaviour
{
public static WorldManager Singleton { get; private set; }
public const float MinMaxDrawDistance = 250f;
public const float MaxMaxDrawDistance = 1500f;
[SerializeField]
[Range(MinMaxDrawDistance, MaxMaxDrawDistance)]
private float _defaultMaxDrawDistance = 1500f;
[SerializeField]
[Range(MinMaxDrawDistance, MaxMaxDrawDistance)]
private float _defaultMaxDrawDistanceOnMobile = 1000f;
private float _maxDrawDistance = 0f;
public float MaxDrawDistance
{
get => _maxDrawDistance;
set
{
if (value == _maxDrawDistance)
return;
_maxDrawDistance = value;
this.ApplyMaxDrawDistance(Cell.Instance);
}
}
private void Awake()
{
Singleton = this;
_maxDrawDistance = Application.isMobilePlatform ? _defaultMaxDrawDistanceOnMobile : _defaultMaxDrawDistance;
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += (s1, s2) => OnActiveSceneChanged();
}
void OnActiveSceneChanged()
{
// apply settings
// we need to find Cell with FindObjectOfType(), because it's Awake() method may have not been called yet
Cell cell = Object.FindObjectOfType<Cell>();
this.ApplyMaxDrawDistance(cell);
}
void ApplyMaxDrawDistance(Cell cell)
{
if (cell != null)
cell.MaxDrawDistance = this.MaxDrawDistance;
var cam = Camera.main;
if (cam != null)
cam.farClipPlane = this.MaxDrawDistance;
cam = Camera.current;
if (cam != null)
cam.farClipPlane = this.MaxDrawDistance;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9dd4217c14fd11f4e8b46fe4a95f7aa5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,895 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SanAndreasUnity.Utilities;
using UnityEngine;
namespace SanAndreasUnity.Behaviours.World
{
public struct WorldSystemParams
{
public uint worldSize;
public ushort numAreasPerAxis;
}
public class WorldSystemWithDistanceLevels<T>
{
private readonly WorldSystem<T>[] _worldSystems;
public IReadOnlyList<WorldSystem<T>> WorldSystems => _worldSystems;
private readonly float[] _distanceLevels;
public IReadOnlyList<float> DistanceLevels => _distanceLevels;
private readonly Dictionary<long, WorldSystem<T>.FocusPoint[]> _focusPointsPerLevel = new Dictionary<long, WorldSystem<T>.FocusPoint[]>();
private long _lastFocusPointId = 1;
public WorldSystemWithDistanceLevels(
float[] distanceLevels,
WorldSystemParams[] worldSystemParamsXZ,
WorldSystemParams[] worldSystemParamsY,
System.Action<WorldSystem<T>.Area, bool> onAreaChangedVisibility)
{
int num = distanceLevels.Length;
if (num != worldSystemParamsXZ.Length || num != worldSystemParamsY.Length)
throw new ArgumentException("Input arrays must be of same size");
if (num == 0)
throw new ArgumentException("You must specify distance levels");
if (!distanceLevels.OrderBy(l => l).SequenceEqual(distanceLevels))
throw new ArgumentException("Input arrays must be sorted ascending by distance level");
if (distanceLevels.Distinct().Count() != distanceLevels.Length)
throw new ArgumentException("Distance levels must be distinct");
_distanceLevels = distanceLevels.ToArray();
_worldSystems = new WorldSystem<T>[num];
for (int i = 0; i < num; i++)
{
_worldSystems[i] = new WorldSystem<T>(worldSystemParamsXZ[i], worldSystemParamsY[i], onAreaChangedVisibility);
}
}
public int GetLevelIndexFromDrawDistance(float drawDistance)
{
if (drawDistance <= 0)
return 0;
int index = System.Array.FindIndex(_distanceLevels, f => f > drawDistance);
if (index >= 0)
return index;
// draw distance is higher than all levels
// these object should go into last level
return _distanceLevels.Length - 1;
}
public long RegisterFocusPoint(float radius, Vector3 pos)
{
var focusPoints = new WorldSystem<T>.FocusPoint[_worldSystems.Length];
for (int i = 0; i < _worldSystems.Length; i++)
focusPoints[i] = _worldSystems[i].RegisterFocusPoint(Mathf.Min(radius, _distanceLevels[i]), pos);
long id = _lastFocusPointId++;
_focusPointsPerLevel[id] = focusPoints;
return id;
}
public void UnRegisterFocusPoint(long id)
{
if (!_focusPointsPerLevel.TryGetValue(id, out var focusPoints))
return;
for (int i = 0; i < _worldSystems.Length; i++)
_worldSystems[i].UnRegisterFocusPoint(focusPoints[i]);
}
public void FocusPointChangedPosition(long id, Vector3 newPos)
{
if (!_focusPointsPerLevel.TryGetValue(id, out var focusPoints))
return;
Vector3 diff = focusPoints[0].Position - newPos;
if (diff.x == 0f && diff.y == 0f && diff.z == 0f) // faster than calling '==' operator
return;
for (int i = 0; i < _worldSystems.Length; i++)
_worldSystems[i].FocusPointChangedPosition(focusPoints[i], newPos);
}
public void FocusPointChangedRadius(long id, float newRadius)
{
if (!_focusPointsPerLevel.TryGetValue(id, out var focusPoints))
return;
for (int i = 0; i < _worldSystems.Length; i++)
_worldSystems[i].FocusPointChangedRadius(focusPoints[i], Mathf.Min(newRadius, _distanceLevels[i]));
}
public void AddObjectToArea(Vector3 pos, float drawDistance, T obj)
{
int levelIndex = this.GetLevelIndexFromDrawDistance(drawDistance);
_worldSystems[levelIndex].AddObjectToArea(pos, obj);
}
public void RemoveObjectFromArea(Vector3 pos, float drawDistance, T obj)
{
int levelIndex = this.GetLevelIndexFromDrawDistance(drawDistance);
_worldSystems[levelIndex].RemoveObjectFromArea(pos, obj);
}
public void Update()
{
for (int i = 0; i < _worldSystems.Length; i++)
_worldSystems[i].Update();
}
}
public class WorldSystem<T>
{
public struct AreaIndex
{
public short x, y, z;
public AreaIndex(short x, short y, short z)
{
this.x = x;
this.y = y;
this.z = z;
}
public bool IsEqualTo(AreaIndex other) => this.x == other.x && this.y == other.y && this.z == other.z;
public override string ToString() => $"({this.x}, {this.y}, {this.z})";
}
public class Area
{
private static long s_lastId = 0;
public long Id { get; } = ++s_lastId;
public AreaIndex AreaIndex { get; }
public WorldSystem<T> WorldSystem { get; }
internal List<T> objectsInside;
public IReadOnlyList<T> ObjectsInside => this.objectsInside;
internal HashSet<FocusPoint> focusPointsThatSeeMe;
public IReadOnlyCollection<FocusPoint> FocusPointsThatSeeMe => this.focusPointsThatSeeMe;
internal bool isMarkedForUpdate;
public bool WasVisibleInLastUpdate { get; internal set; } = false;
internal Area(WorldSystem<T> worldSystem, AreaIndex areaIndex)
{
this.WorldSystem = worldSystem;
this.AreaIndex = areaIndex;
}
}
public sealed class FocusPoint
{
private static long _lastId = 1;
public long Id { get; } = _lastId++;
public float Radius { get; internal set; }
public Vector3 Position { get; internal set; }
internal FocusPoint()
{
}
public override int GetHashCode()
{
return this.Id.GetHashCode();
}
}
private struct Range
{
public short lower, higher;
public short Length => (short) (this.higher - this.lower);
public Range(short lower, short higher)
{
if (lower > higher)
throw new ArgumentException($"lower {lower} is > than higher {higher}");
this.lower = lower;
this.higher = higher;
}
public bool EqualsToOther(Range other) => this.lower == other.lower && this.higher == other.higher;
public bool Overlaps(Range other) => !(this.higher < other.lower || this.lower > other.higher);
public bool IsInsideOf(Range other) => (this.lower >= other.lower && this.higher < other.higher)
|| (this.lower > other.lower && this.higher <= other.higher);
public bool IsEqualOrInsideOf(Range other) => this.EqualsToOther(other) || this.IsInsideOf(other);
}
private struct AreaIndexes
{
public Range x, y, z;
public Range this[int index]
{
get
{
switch (index)
{
case 0:
return this.x;
case 1:
return this.y;
case 2:
return this.z;
default:
throw new System.IndexOutOfRangeException("Invalid index");
}
}
}
public int Volume => (this.x.Length + 1) * (this.y.Length + 1) * (this.z.Length + 1);
public bool EqualsToOther(AreaIndexes other) => this.x.EqualsToOther(other.x) && this.y.EqualsToOther(other.y) && this.z.EqualsToOther(other.z);
public bool Overlaps(AreaIndexes other) => this.x.Overlaps(other.x) && this.y.Overlaps(other.y) && this.z.Overlaps(other.z);
public bool IsInsideOf(AreaIndexes other) => !this.EqualsToOther(other)
&& (this.x.IsEqualOrInsideOf(other.x) && this.y.IsEqualOrInsideOf(other.y) && this.z.IsEqualOrInsideOf(other.z));
}
// private struct NewAreasResult
// {
// public (AreaIndexes areaIndexes, bool hasResult) x, y, z;
//
// public static NewAreasResult WithOne(AreaIndexes areaIndexes) => new NewAreasResult { x = (areaIndexes, true) };
// }
private struct AffectedRangesForAxis
{
// there can be max 3 ranges
// each range can be intersection, or free
// there can be max 2 free parts and max 1 intersection part
public (Range range, bool isIntersectionPart, bool hasValues) range1, range2, range3;
// is there intersection on this axis ? if not, cubes do not intersect, and other results from this struct should be ignored
public bool hasIntersectionOnAxis;
public IEnumerable<(Range range, bool isIntersectionPart, bool hasValues)> Ranges => new [] {range1, range2, range3};
public void ForEachWithValue(Action<(Range range, bool isIntersectionPart)> action)
{
if (range1.hasValues)
action((range1.range, range1.isIntersectionPart));
if (range2.hasValues)
action((range2.range, range2.isIntersectionPart));
if (range3.hasValues)
action((range3.range, range3.isIntersectionPart));
}
public void ForEachFree(Action<Range> action)
{
if (range1.hasValues && !range1.isIntersectionPart)
action(range1.range);
if (range2.hasValues && !range2.isIntersectionPart)
action(range2.range);
if (range3.hasValues && !range3.isIntersectionPart)
action(range3.range);
}
}
private struct AxisInfo
{
public float worldMin;
public float worldMax;
public float worldHalfSize;
public float areaSize;
public ushort numAreasPerAxis;
}
public class ConcurrentModificationException : System.Exception
{
public ConcurrentModificationException()
: base("Can not perform the operation because it would result in concurrent modification of collections")
{
}
}
private readonly Area[,,] _areas;
public int GetNumAreas(int axisIndex) => _areas.GetLength(axisIndex);
private readonly HashSet<FocusPoint> _focusPoints = new HashSet<FocusPoint>();
public IReadOnlyCollection<FocusPoint> FocusPoints => _focusPoints;
private readonly List<Area> _areasForUpdate = new List<Area>(128);
private readonly AxisInfo _xzAxisInfo;
private readonly AxisInfo _yAxisInfo;
private bool _isInUpdate = false;
// these buffers are reused every time to avoid memory allocations, but that makes this class non thread safe
private AreaIndexes[] _bufferForGettingNewAreas = new AreaIndexes[27]; // 3^3
private AreaIndexes[] _bufferForGettingOldAreas = new AreaIndexes[27]; // 3^3
private readonly System.Action<Area, bool> _onAreaChangedVisibility = null;
public WorldSystem(
WorldSystemParams worldSystemParamsXZ,
WorldSystemParams worldSystemParamsY,
System.Action<Area, bool> onAreaChangedVisibility)
{
_xzAxisInfo = CalculateAxisInfo(worldSystemParamsXZ);
_yAxisInfo = CalculateAxisInfo(worldSystemParamsY);
_areas = new Area[_xzAxisInfo.numAreasPerAxis, _yAxisInfo.numAreasPerAxis, _xzAxisInfo.numAreasPerAxis];
_onAreaChangedVisibility = onAreaChangedVisibility;
}
private static AxisInfo CalculateAxisInfo(WorldSystemParams worldSystemParams)
{
if (worldSystemParams.worldSize <= 0)
throw new ArgumentException("World size must be higher than 0");
ushort maxNumAreasPerAxis = ushort.MaxValue - 10;
if (worldSystemParams.numAreasPerAxis > maxNumAreasPerAxis)
throw new ArgumentException($"Num areas per axis can not be higher than {maxNumAreasPerAxis}");
AxisInfo axisInfo = new AxisInfo();
axisInfo.worldMin = - worldSystemParams.worldSize / 2f;
axisInfo.worldMax = worldSystemParams.worldSize / 2f;
axisInfo.worldHalfSize = worldSystemParams.worldSize / 2f;
axisInfo.areaSize = worldSystemParams.worldSize / (float)worldSystemParams.numAreasPerAxis;
axisInfo.numAreasPerAxis = (ushort) (worldSystemParams.numAreasPerAxis + 2); // additional 2 for positions out of bounds
return axisInfo;
}
public FocusPoint RegisterFocusPoint(float radius, Vector3 pos)
{
this.ThrowIfConcurrentModification();
if (radius < 0)
throw new ArgumentException("Radius can not be < 0");
var focusPoint = new FocusPoint();
focusPoint.Radius = radius;
focusPoint.Position = pos;
if (!_focusPoints.Add(focusPoint))
throw new Exception("Failed to add focus point to the collection");
this.ForEachAreaInRadius(pos, radius, true, area =>
{
AddToFocusPointsThatSeeMe(area, focusPoint);
this.MarkAreaForUpdate(area);
});
return focusPoint;
}
public void UnRegisterFocusPoint(FocusPoint focusPoint)
{
this.ThrowIfConcurrentModification();
if (!_focusPoints.Remove(focusPoint))
return;
this.ForEachAreaInRadius(focusPoint.Position, focusPoint.Radius, false, area =>
{
if (null == area)
return;
RemoveFromFocusPointsThatSeeMe(area, focusPoint);
this.MarkAreaForUpdate(area);
});
}
public void FocusPointChangedPosition(FocusPoint focusPoint, Vector3 newPos)
=> this.FocusPointChangedParameters(focusPoint, newPos, focusPoint.Radius);
private void FocusPointChangedParameters(FocusPoint focusPoint, Vector3 newPos, float newRadius)
{
this.ThrowIfConcurrentModification();
AreaIndexes oldIndexes = GetAreaIndexesInRadius(focusPoint.Position, focusPoint.Radius);
AreaIndexes newIndexes = GetAreaIndexesInRadius(newPos, newRadius);
if (!oldIndexes.EqualsToOther(newIndexes))
{
// areas changed
byte numNewAreaIndexes = GetNewAreas(oldIndexes, newIndexes, _bufferForGettingNewAreas);
byte numOldAreaIndexes = GetNewAreas(newIndexes, oldIndexes, _bufferForGettingOldAreas);
for (byte i = 0; i < numNewAreaIndexes; i++)
{
var areaIndexes = _bufferForGettingNewAreas[i];
this.ForEachArea(areaIndexes, true, area =>
{
// this can happen multiple times per single area, but since we use hashset it should be no problem
// actually, it should not happen anymore with new implementation
AddToFocusPointsThatSeeMe(area, focusPoint);
this.MarkAreaForUpdate(area);
});
}
for (byte i = 0; i < numOldAreaIndexes; i++)
{
var areaIndexes = _bufferForGettingOldAreas[i];
this.ForEachArea(areaIndexes, false, area =>
{
if (null == area)
return;
RemoveFromFocusPointsThatSeeMe(area, focusPoint);
this.MarkAreaForUpdate(area);
});
}
}
focusPoint.Position = newPos;
focusPoint.Radius = newRadius;
}
public void FocusPointChangedRadius(FocusPoint focusPoint, float newRadius)
=> this.FocusPointChangedParameters(focusPoint, focusPoint.Position, newRadius);
public void AddObjectToArea(Vector3 pos, T obj)
{
this.ThrowIfConcurrentModification();
var area = GetAreaAt(pos, true);
if (null == area.objectsInside)
area.objectsInside = new List<T>();
area.objectsInside.Add(obj);
}
public void RemoveObjectFromArea(Vector3 pos, T obj)
{
this.ThrowIfConcurrentModification();
var area = GetAreaAt(pos, false);
if (area != null && area.objectsInside != null)
{
area.objectsInside.Remove(obj);
}
}
public void Update()
{
this.ThrowIfConcurrentModification();
_isInUpdate = true;
try
{
this.UpdateInternal();
}
finally
{
_isInUpdate = false;
}
}
private void UpdateInternal()
{
// check areas that are marked for update
for (int i = 0; i < _areasForUpdate.Count; i++)
{
var area = _areasForUpdate[i];
if (!area.isMarkedForUpdate) // should not happen, but just in case
continue;
bool isVisible = ShouldAreaBeVisible(area);
this.NotifyAreaChangedVisibility(area, isVisible);
area.WasVisibleInLastUpdate = isVisible;
area.isMarkedForUpdate = false;
}
_areasForUpdate.Clear();
}
public void ForEachAreaInRadius(Vector3 pos, float radius, System.Action<Area> action)
{
this.ForEachAreaInRadius(pos, radius, false, action);
}
private void ForEachAreaInRadius(Vector3 pos, float radius, bool createIfNotExists, System.Action<Area> action)
{
this.ForEachArea(GetAreaIndexesInRadius(pos, radius), createIfNotExists, action);
}
public List<Area> GetAreasInRadius(Vector3 pos, float radius)
{
var areas = new List<Area>();
this.ForEachAreaInRadius(pos, radius, false, a => areas.Add(a));
return areas;
}
private void ForEachArea(AreaIndexes areaIndexes, bool createIfNotExists, System.Action<Area> action)
{
for (short x = areaIndexes.x.lower; x <= areaIndexes.x.higher; x++)
{
for (short y = areaIndexes.y.lower; y <= areaIndexes.y.higher; y++)
{
for (short z = areaIndexes.z.lower; z <= areaIndexes.z.higher; z++)
{
if (!createIfNotExists)
{
action(_areas[x, y, z]);
}
else
{
var area = _areas[x, y, z];
if (null == area)
_areas[x, y, z] = area = new Area(this, new AreaIndex(x, y, z));
action(area);
}
}
}
}
}
private AreaIndexes GetAreaIndexesInRadius(Vector3 pos, float radius)
{
if (radius < 0)
throw new ArgumentException("Radius can not be < 0");
// Vector3 min = new Vector3(pos.x - radius, pos.y - radius, pos.z - radius);
// Vector3 max = new Vector3(pos.x + radius, pos.y + radius, pos.z + radius);
return new AreaIndexes
{
x = new Range { lower = GetAreaIndex(pos.x - radius), higher = GetAreaIndex(pos.x + radius) },
y = new Range { lower = GetAreaIndexForYAxis(pos.y - radius), higher = GetAreaIndexForYAxis(pos.y + radius) },
z = new Range { lower = GetAreaIndex(pos.z - radius), higher = GetAreaIndex(pos.z + radius) },
};
}
private short GetAreaIndex(float pos)
{
if (pos < _xzAxisInfo.worldMin)
return 0;
if (pos > _xzAxisInfo.worldMax)
return (short) (_xzAxisInfo.numAreasPerAxis - 1);
// skip 1st
return (short) (1 + Mathf.FloorToInt((pos + _xzAxisInfo.worldHalfSize) / _xzAxisInfo.areaSize));
}
private short GetAreaIndexForYAxis(float pos)
{
if (pos < _yAxisInfo.worldMin)
return 0;
if (pos > _yAxisInfo.worldMax)
return (short) (_yAxisInfo.numAreasPerAxis - 1);
// skip 1st
return (short) (1 + Mathf.FloorToInt((pos + _yAxisInfo.worldHalfSize) / _yAxisInfo.areaSize));
}
private AreaIndex GetAreaIndex(Vector3 pos)
{
return new AreaIndex(GetAreaIndex(pos.x), GetAreaIndexForYAxis(pos.y), GetAreaIndex(pos.z));
}
public Vector3 GetAreaCenter(Area area)
{
Vector3 center = new Vector3(
GetAreaCenterForXZAxis(area.AreaIndex.x),
GetAreaCenterForYAxis(area.AreaIndex.y),
GetAreaCenterForXZAxis(area.AreaIndex.z));
AreaIndex indexOfCenter = GetAreaIndex(center);
if (!indexOfCenter.IsEqualTo(area.AreaIndex)) // just to be sure
throw new Exception($"Index of area center {indexOfCenter} does not match original area index {area.AreaIndex}");
return center;
}
private float GetAreaCenterForXZAxis(short indexForAxis)
{
if (indexForAxis <= 0) // left infinity
return float.NegativeInfinity;
if (indexForAxis >= _xzAxisInfo.numAreasPerAxis - 1) // right infinity
return float.PositiveInfinity;
return _xzAxisInfo.areaSize * (indexForAxis - 1) - _xzAxisInfo.worldHalfSize + _xzAxisInfo.areaSize * 0.5f;
}
private float GetAreaCenterForYAxis(short indexForAxis)
{
if (indexForAxis <= 0) // left infinity
return float.NegativeInfinity;
if (indexForAxis >= _yAxisInfo.numAreasPerAxis - 1) // right infinity
return float.PositiveInfinity;
return _yAxisInfo.areaSize * (indexForAxis - 1) - _yAxisInfo.worldHalfSize + _yAxisInfo.areaSize * 0.5f;
}
private Area GetAreaAt(Vector3 pos, bool createIfNotExists)
{
var index = this.GetAreaIndex(pos);
var area = _areas[index.x, index.y, index.z];
if (null == area && createIfNotExists)
{
area = new Area(this, index);
_areas[index.x, index.y, index.z] = area;
}
return area;
}
private byte GetNewAreas(AreaIndexes oldIndexes, AreaIndexes newIndexes, AreaIndexes[] resultBuffer)
{
var xResult = GetAffectedRangesForAxis(oldIndexes.x, newIndexes.x);
ValidateAffectedRangesForAxis(xResult);
if (!xResult.hasIntersectionOnAxis)
{
resultBuffer[0] = newIndexes;
return 1;
}
var yResult = GetAffectedRangesForAxis(oldIndexes.y, newIndexes.y);
ValidateAffectedRangesForAxis(yResult);
if (!yResult.hasIntersectionOnAxis)
{
resultBuffer[0] = newIndexes;
return 1;
}
var zResult = GetAffectedRangesForAxis(oldIndexes.z, newIndexes.z);
ValidateAffectedRangesForAxis(zResult);
if (!zResult.hasIntersectionOnAxis)
{
resultBuffer[0] = newIndexes;
return 1;
}
// for all combinations, if at least 1 range is free
byte count = 0;
xResult.ForEachWithValue(tupleX =>
{
yResult.ForEachWithValue(tupleY =>
{
zResult.ForEachWithValue(tupleZ =>
{
if (!tupleX.isIntersectionPart || !tupleY.isIntersectionPart || !tupleZ.isIntersectionPart)
{
// at least 1 range is free
resultBuffer[count] = new AreaIndexes
{
x = tupleX.range,
y = tupleY.range,
z = tupleZ.range,
};
count++;
}
});
});
});
if (count == 0)
{
// there are no free ranges - new cube is inside of old cube (or equal) - there are no new areas - return 0
if (newIndexes.Volume > oldIndexes.Volume)
throw new Exception("New cube should be <= than old cube");
if (!newIndexes.IsInsideOf(oldIndexes))
throw new Exception("New cube should be inside of old cube");
}
return count;
}
private AffectedRangesForAxis GetAffectedRangesForAxis(
Range oldRange,
Range newRange)
{
if (oldRange.EqualsToOther(newRange))
{
// same position and size along this axis
return new AffectedRangesForAxis
{
hasIntersectionOnAxis = true,
range1 = (oldRange, true, true),
};
}
// check if there is intersection
if (oldRange.lower > newRange.higher)
return default;
if (newRange.lower > oldRange.higher)
return default;
// first find intersection part (max 1)
var toReturn = new AffectedRangesForAxis { hasIntersectionOnAxis = true };
Range totalRange = new Range(
Min(oldRange.lower, newRange.lower),
Max(oldRange.higher, newRange.higher));
short minOfHighers = Min(oldRange.higher, newRange.higher);
Range intersectionRange;
if (oldRange.lower < newRange.lower)
{
// he is left
intersectionRange = new Range(newRange.lower, minOfHighers);
}
else if (oldRange.lower == newRange.lower)
{
// they share left edge
intersectionRange = new Range(newRange.lower, minOfHighers);
}
else
{
// his left edge is more to the right
intersectionRange = new Range(oldRange.lower, minOfHighers);
}
// now find free range(s) based on total range and intersection range
if (intersectionRange.Length >= totalRange.Length) // should not happen
throw new Exception($"Intersection range length {intersectionRange.Length} is >= than total range length {totalRange.Length}");
toReturn.range1 = (intersectionRange, true, true);
if (newRange.EqualsToOther(intersectionRange))
{
// new range is inside of old range
// there are no free ranges
return toReturn;
}
if (newRange.lower >= intersectionRange.lower)
{
Range freeRange = new Range((short) (intersectionRange.higher + 1), newRange.higher);
toReturn.range2 = (freeRange, false, true);
return toReturn;
}
// newRange.lower < intersectionRange.lower
Range freeRange1 = new Range(newRange.lower, (short) (intersectionRange.lower - 1));
toReturn.range2 = (freeRange1, false, true);
if (newRange.higher > intersectionRange.higher)
{
Range freeRange2 = new Range((short) (intersectionRange.higher + 1), newRange.higher);
toReturn.range3 = (freeRange2, false, true);
}
return toReturn;
}
private static void ValidateAffectedRangesForAxis(AffectedRangesForAxis affectedRangesForAxis)
{
int count = affectedRangesForAxis.Ranges.Count(r => r.hasValues);
if (!affectedRangesForAxis.hasIntersectionOnAxis && count > 0)
throw new Exception("Count > 0 and has no intersection");
if (!affectedRangesForAxis.hasIntersectionOnAxis)
return;
var intersectionRanges = affectedRangesForAxis.Ranges
.Where(r => r.hasValues && r.isIntersectionPart)
.ToList();
var freeRanges = affectedRangesForAxis.Ranges
.Where(r => r.hasValues && !r.isIntersectionPart)
.ToList();
if (intersectionRanges.Count != 1)
throw new Exception($"Num intersection ranges must be 1, found {intersectionRanges.Count}");
if (freeRanges.Count > 2)
throw new Exception($"Num free ranges is {freeRanges.Count}");
var allRanges = intersectionRanges.Concat(freeRanges).ToList();
for (int i = 0; i < allRanges.Count; i++)
{
for (int j = i + 1; j < allRanges.Count; j++)
{
var r1 = allRanges[i].range;
var r2 = allRanges[j].range;
if (r1.Overlaps(r2))
throw new Exception($"Ranges overlap, {r1} and {r2}");
}
}
// there must be no space between ranges
var orderedRanges = allRanges.OrderBy(r => r.range.lower).ToList();
for (int i = 0; i < orderedRanges.Count - 1; i++)
{
var r1 = orderedRanges[i].range;
var r2 = orderedRanges[i+1].range;
if (r1.higher + 1 != r2.lower)
throw new Exception($"There is space between ranges, higher is {r1.higher}, lower is {r2.lower}");
}
}
private static void EnsureFocusPointsCollectionInitialized(Area area)
{
if (null == area.focusPointsThatSeeMe)
area.focusPointsThatSeeMe = new HashSet<FocusPoint>();
}
private static bool ShouldAreaBeVisible(Area area)
{
return area.focusPointsThatSeeMe != null && area.focusPointsThatSeeMe.Count > 0;
}
private void MarkAreaForUpdate(Area area)
{
this.ThrowIfConcurrentModification(); // just in case
if (area.isMarkedForUpdate)
return;
if (ShouldAreaBeVisible(area) == area.WasVisibleInLastUpdate) // visibility does not change
return;
_areasForUpdate.Add(area);
area.isMarkedForUpdate = true;
}
private static void AddToFocusPointsThatSeeMe(Area area, FocusPoint focusPoint)
{
EnsureFocusPointsCollectionInitialized(area);
if (!area.focusPointsThatSeeMe.Add(focusPoint))
throw new Exception($"Failed to add focus point with id {focusPoint.Id} - it already exists");
}
private static void RemoveFromFocusPointsThatSeeMe(Area area, FocusPoint focusPoint)
{
bool success = false;
if (area.focusPointsThatSeeMe != null)
{
success = area.focusPointsThatSeeMe.Remove(focusPoint);
if (area.focusPointsThatSeeMe.Count == 0)
area.focusPointsThatSeeMe = null;
}
if (!success)
throw new Exception($"Failed to remove focus point with id {focusPoint.Id} - it doesn't exist");
}
private static short Min(short a, short b)
{
return a <= b ? a : b;
}
private static short Max(short a, short b)
{
return a >= b ? a : b;
}
private void ThrowIfConcurrentModification()
{
if (_isInUpdate)
throw new ConcurrentModificationException();
}
private void NotifyAreaChangedVisibility(Area area, bool visible)
{
F.RunExceptionSafe(() => this._onAreaChangedVisibility(area, visible));
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3a5d19389522a5f4a9c55307d283b0e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,4 +1,7 @@
using System.Reflection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
@ -7,41 +10,82 @@ namespace SanAndreasUnity.Editor
public static class EditorUtils
{
public static void DrawAllFieldsInInspector(object objectToDraw)
private static List<Type> GetWithBaseTypes(Type type, int maxDepth)
{
var fieldInfos = objectToDraw
.GetType()
.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
var types = new List<Type>();
types.Add(type);
type = type.BaseType;
for (int i = 0; i < maxDepth; i++)
{
if (type == null)
break;
types.Add(type);
type = type.BaseType;
}
return types;
}
public static void DrawFieldsAndPropertiesInInspector(object objectToDraw, int inheritanceLevel)
{
DrawFieldsInInspector(objectToDraw, inheritanceLevel);
DrawPropertiesInInspector(objectToDraw, inheritanceLevel);
}
public static void DrawFieldsInInspector(object objectToDraw, int inheritanceLevel)
{
var fieldInfos = GetWithBaseTypes(objectToDraw.GetType(), inheritanceLevel)
.SelectMany(t => t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly));
foreach (var fieldInfo in fieldInfos)
{
var type = fieldInfo.FieldType;
var value = fieldInfo.GetValue(objectToDraw);
string labelText = $"{fieldInfo.Name}: ";
DrawObjectInInspector(fieldInfo.FieldType, fieldInfo.GetValue(objectToDraw), fieldInfo.Name);
}
}
if (type.IsAssignableFrom(typeof(Component)))
{
EditorGUILayout.ObjectField(labelText, value as Component, type, true);
}
else if (type.IsEnum)
{
if (type.GetCustomAttribute<System.FlagsAttribute>() != null)
EditorGUILayout.EnumFlagsField(labelText, value as System.Enum);
else
EditorGUILayout.EnumPopup(labelText, value as System.Enum);
}
else if (type == typeof(Color))
{
EditorGUILayout.ColorField(labelText, (Color) value);
}
else if (type == typeof(Color32))
{
EditorGUILayout.ColorField(labelText, (Color32) value);
}
public static void DrawPropertiesInInspector(object objectToDraw, int inheritanceLevel)
{
var properties = GetWithBaseTypes(objectToDraw.GetType(), inheritanceLevel)
.SelectMany(t => t.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly))
.Where(p => p.CanRead);
foreach (var propertyInfo in properties)
{
DrawObjectInInspector(propertyInfo.PropertyType, propertyInfo.GetValue(objectToDraw), propertyInfo.Name);
}
}
public static void DrawObjectInInspector(
Type type,
object value,
string name)
{
string labelText = $"{name}: ";
if (typeof(Component).IsAssignableFrom(type))
{
EditorGUILayout.ObjectField(labelText, value as Component, type, true);
}
else if (type.IsEnum)
{
if (type.GetCustomAttribute<System.FlagsAttribute>() != null)
EditorGUILayout.EnumFlagsField(labelText, value as System.Enum);
else
{
EditorGUILayout.LabelField($"{labelText} {value}");
}
EditorGUILayout.EnumPopup(labelText, value as System.Enum);
}
else if (type == typeof(Color))
{
EditorGUILayout.ColorField(labelText, (Color) value);
}
else if (type == typeof(Color32))
{
EditorGUILayout.ColorField(labelText, (Color32) value);
}
else
{
EditorGUILayout.LabelField($"{labelText} {value}");
}
}
}

View file

@ -18,7 +18,7 @@ namespace SanAndreasUnity.Editor
var lightSource = (LightSource) this.target;
EditorUtils.DrawAllFieldsInInspector(lightSource.LightInfo);
EditorUtils.DrawFieldsInInspector(lightSource.LightInfo, 0);
}
}

View file

@ -0,0 +1,44 @@
using SanAndreasUnity.Behaviours.World;
using UnityEditor;
using UnityEngine;
namespace SanAndreasUnity.Editor
{
[CustomEditor(typeof(StaticGeometry))]
public class StaticGeometryInspector : UnityEditor.Editor
{
private Vector2 _scrollViewPos;
public override void OnInspectorGUI()
{
base.DrawDefaultInspector ();
GUILayout.Space (10);
GUILayout.Label("Info:");
GUILayout.Space (10);
var staticGeometry = (StaticGeometry) this.target;
_scrollViewPos = EditorGUILayout.BeginScrollView(_scrollViewPos, GUILayout.MinHeight(350));
EditorUtils.DrawPropertiesInInspector(staticGeometry, 1);
GUILayout.Space (10);
GUILayout.Label("Object definition:");
GUILayout.Space (10);
if (staticGeometry.ObjectDefinition != null)
EditorUtils.DrawFieldsAndPropertiesInInspector(staticGeometry.ObjectDefinition, 0);
GUILayout.Space (10);
GUILayout.Label("Placement info:");
GUILayout.Space (10);
if (staticGeometry.Instance != null)
EditorUtils.DrawFieldsAndPropertiesInInspector(staticGeometry.Instance, 0);
EditorGUILayout.EndScrollView();
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a459375e4c87f3f4fa07de1ba6ca67a2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -151,9 +151,10 @@ namespace SanAndreasUnity.Importing.Archive
// this method should not be synchronized, because thread would block while
// archive is being read, but the thread only wants to register a job and continue
// [MethodImpl(MethodImplOptions.Synchronized)]
public static void ReadFileAsync(string name, System.Action<Stream> onFinish)
public static void ReadFileAsync(string name, float loadPriority, System.Action<Stream> onFinish)
{
Behaviours.LoadingThread.RegisterJob (new Behaviours.LoadingThread.Job<Stream> () {
priority = loadPriority,
action = () => ReadFile( name ),
callbackFinish = (stream) => { onFinish(stream); },
});

View file

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Text;
using UnityEngine;
using Profiler = UnityEngine.Profiling.Profiler;
namespace SanAndreasUnity.Importing.Collision
{
@ -100,10 +101,10 @@ namespace SanAndreasUnity.Importing.Collision
}
}
private static void LoadAsync (string collFileName, CollisionFileInfo collFileInfo, System.Action<CollisionFile> onFinish)
private static void LoadAsync (string collFileName, CollisionFileInfo collFileInfo, float loadPriority, System.Action<CollisionFile> onFinish)
{
ArchiveManager.ReadFileAsync (collFileName, (stream) => {
ArchiveManager.ReadFileAsync (collFileName, loadPriority, (stream) => {
CollisionFile cf = null;
try {
using(stream)
@ -128,19 +129,19 @@ namespace SanAndreasUnity.Importing.Collision
public static CollisionFile FromName(String name)
{
if (s_asyncLoader.IsObjectLoaded (name))
return s_asyncLoader.GetLoadedObject (name);
if (s_asyncLoader.TryGetLoadedObject(name, out CollisionFile alreadyLoadedCollisionFile))
return alreadyLoadedCollisionFile;
UnityEngine.Profiling.Profiler.BeginSample ("CollisionFile.FromName()");
var cf = _sModelNameDict.ContainsKey(name) ? _sModelNameDict[name].Value : null;
UnityEngine.Profiling.Profiler.EndSample ();
s_asyncLoader.AddToLoadedObjects (name, cf);
s_asyncLoader.OnObjectFinishedLoading(name, cf, true);
return cf;
}
public static void FromNameAsync(String name, System.Action<CollisionFile> onFinish)
public static void FromNameAsync(String name, float loadPriority, System.Action<CollisionFile> onFinish)
{
if (!_sModelNameDict.ContainsKey (name))
{
@ -158,7 +159,7 @@ namespace SanAndreasUnity.Importing.Collision
// get the actual name of collision file
string collFileName = _sModelNameDict [name].FileName;
LoadAsync( collFileName, _sModelNameDict[name], (result) =>
LoadAsync( collFileName, _sModelNameDict[name], loadPriority, (result) =>
{
// update _value variable in appropriate CollisionFileInfo object
_sModelNameDict[name].Value = result;
@ -185,6 +186,8 @@ namespace SanAndreasUnity.Importing.Collision
private CollisionFile(CollisionFileInfo info, Stream stream)
{
Profiler.BeginSample("CollisionFile()");
Name = info.Name;
ModelId = info.ModelId;
@ -316,6 +319,8 @@ namespace SanAndreasUnity.Importing.Collision
{
Vertices = new Vertex[0];
}
Profiler.EndSample();
}
}
}

View file

@ -108,7 +108,7 @@ namespace SanAndreasUnity.Importing.Conversion
col.Spawn(destParent, forceConvex);
}
public static void LoadAsync(string name, CollisionFile file, Transform destParent, bool forceConvex, System.Action onFinish)
public static void LoadAsync(string name, CollisionFile file, Transform destParent, bool forceConvex, float loadPriority, System.Action onFinish)
{
// load collision file asyncly, and when it's ready just call the other function
@ -123,7 +123,7 @@ namespace SanAndreasUnity.Importing.Conversion
}
// load collision file asyncly
CollisionFile.FromNameAsync (name, (cf) => {
CollisionFile.FromNameAsync (name, loadPriority, (cf) => {
// loading finished
// call other function
if(cf != null)

View file

@ -6,8 +6,9 @@ using SanAndreasUnity.Importing.RenderWareStream;
using System;
using System.Collections.Generic;
using System.Linq;
using SanAndreasUnity.Utilities;
using UnityEngine;
using UnityEngine.Profiling;
using Profiler = UnityEngine.Profiling.Profiler;
namespace SanAndreasUnity.Importing.Conversion
{
@ -398,6 +399,8 @@ namespace SanAndreasUnity.Importing.Conversion
public GeometryParts(string name, Clump clump, TextureDictionary[] txds)
{
Profiler.BeginSample("GeometryParts()");
Name = name;
Geometry = clump.GeometryList.Geometry
@ -409,6 +412,8 @@ namespace SanAndreasUnity.Importing.Conversion
.ToArray();
_collisions = clump.Collision;
Profiler.EndSample();
}
public void AttachCollisionModel(Transform destParent, bool forceConvex = false)
@ -522,34 +527,32 @@ namespace SanAndreasUnity.Importing.Conversion
return Load(modelName, texDictNames.Select(x => TextureDictionary.Load(x)).ToArray());
}
public static void LoadAsync(string modelName, string[] texDictNames, System.Action<GeometryParts> onFinish)
public static void LoadAsync(string modelName, string[] texDictNames, float loadPriority, System.Action<GeometryParts> onFinish)
{
// load each texture asyncly (or load them all at once ?)
// copy array to local variable
texDictNames = texDictNames.ToArray ();
// copy array to local variable
texDictNames = texDictNames.Length > 0 ? texDictNames.ToArray() : Array.Empty<string>();
if (0 == texDictNames.Length)
{
LoadAsync( modelName, new TextureDictionary[0], onFinish );
LoadAsync( modelName, Array.Empty<TextureDictionary>(), loadPriority, onFinish );
return;
}
// requesting a load for both clump and TXD on the same frame will not give much better performance (probably),
// so no need to do it
var loadedTextDicts = new List<TextureDictionary> ();
for (int i = 0; i < texDictNames.Length; i++)
{
// bool isLast = i == texDictNames.Length - 1;
TextureDictionary.LoadAsync (texDictNames [i], (texDict) => {
TextureDictionary.LoadAsync (texDictNames [i], loadPriority, (texDict) => {
loadedTextDicts.Add (texDict);
if (loadedTextDicts.Count == texDictNames.Length)
{
// finished loading all tex dicts
LoadAsync (modelName, loadedTextDicts.ToArray (), onFinish);
LoadAsync (modelName, loadedTextDicts.ToArray (), loadPriority, onFinish);
}
});
}
@ -558,11 +561,11 @@ namespace SanAndreasUnity.Importing.Conversion
public static GeometryParts Load(string modelName, params TextureDictionary[] txds)
{
modelName = modelName.ToLower();
modelName = modelName.ToLowerIfNotLower();
if (s_asyncLoader.IsObjectLoaded(modelName))
if (s_asyncLoader.TryGetLoadedObject(modelName, out GeometryParts alreadyLoadedObject))
{
return s_asyncLoader.GetLoadedObject (modelName);
return alreadyLoadedObject;
}
Profiler.BeginSample ("ReadClump");
@ -578,14 +581,14 @@ namespace SanAndreasUnity.Importing.Conversion
var loaded = new GeometryParts(modelName, clump, txds);
Profiler.EndSample ();
s_asyncLoader.AddToLoadedObjects (modelName, loaded);
s_asyncLoader.OnObjectFinishedLoading(modelName, loaded, true);
return loaded;
}
public static void LoadAsync(string modelName, TextureDictionary[] txds, System.Action<GeometryParts> onFinish)
public static void LoadAsync(string modelName, TextureDictionary[] txds, float loadPriority, System.Action<GeometryParts> onFinish)
{
modelName = modelName.ToLower();
modelName = modelName.ToLowerIfNotLower();
if (!s_asyncLoader.TryLoadObject (modelName, onFinish))
{
@ -597,6 +600,7 @@ namespace SanAndreasUnity.Importing.Conversion
GeometryParts loadedGeoms = null;
LoadingThread.RegisterJob (new LoadingThread.Job<Clump> () {
priority = loadPriority,
action = () => {
// read archive file in background thread
var clump = ArchiveManager.ReadFile<Clump>(modelName + ".dff");
@ -656,8 +660,8 @@ namespace SanAndreasUnity.Importing.Conversion
matFlags |= MaterialFlags.NoBackCull;
}
if ((flags & (ObjectFlag.Alpha1 | ObjectFlag.Alpha2)) != 0
&& (flags & ObjectFlag.DisableShadowMesh) == ObjectFlag.DisableShadowMesh)
if ((flags & (ObjectFlag.DrawLast | ObjectFlag.Additive)) != 0
&& (flags & ObjectFlag.NoZBufferWrite) == ObjectFlag.NoZBufferWrite)
{
matFlags |= MaterialFlags.Alpha;
}

View file

@ -255,20 +255,21 @@ namespace SanAndreasUnity.Importing.Conversion
public static TextureDictionary Load(string name)
{
name = name.ToLower();
if (s_asyncLoader.IsObjectLoaded (name))
return s_asyncLoader.GetLoadedObject (name);
if (s_asyncLoader.TryGetLoadedObject(name, out var alreadyLoadedTxd))
return alreadyLoadedTxd;
UnityEngine.Profiling.Profiler.BeginSample ("TextureDictionary.Load");
var txd = new TextureDictionary(DontLoadTextures ? null : ArchiveManager.ReadFile<RenderWareStream.TextureDictionary>(name + ".txd"));
s_asyncLoader.AddToLoadedObjects(name, txd);
s_asyncLoader.OnObjectFinishedLoading(name, txd, true);
UnityEngine.Profiling.Profiler.EndSample ();
return txd;
}
public static void LoadAsync(string name, System.Action<TextureDictionary> onFinish)
public static void LoadAsync(string name, float loadPriority, System.Action<TextureDictionary> onFinish)
{
name = name.ToLower();
@ -282,6 +283,7 @@ namespace SanAndreasUnity.Importing.Conversion
bool bDontLoad = DontLoadTextures;
Behaviours.LoadingThread.RegisterJob (new Behaviours.LoadingThread.Job<RenderWareStream.TextureDictionary> () {
priority = loadPriority,
action = () => {
return bDontLoad ? null : ArchiveManager.ReadFile<RenderWareStream.TextureDictionary>(name + ".txd");
},

View file

@ -6,23 +6,26 @@ namespace SanAndreasUnity.Importing.Items.Definitions
public enum ObjectFlag : uint
{
None = 0,
WetEffect = 1,
RenderAtNight = 2,
Alpha1 = 4,
Alpha2 = 8,
RenderAtDay = 16,
Interior = 32,
DisableShadowMesh = 64,
NoCull = 128,
DisableDrawDist = 256,
Breakable = 512,
BreakableCrack = 1024,
IsRoad = 1,
RenderAtNight = 2, // ?
DrawLast = 4,
Additive = 8,
RenderAtDay = 16, // ?
Interior = 32, // ? doors
NoZBufferWrite = 64,
DontReceiveShadows = 128,
DisableDrawDist = 256, // ?
Breakable = 512, // IS_GLASS_TYPE_1
BreakableCrack = 1024, // IS_GLASS_TYPE_2
GarageDoor = 2048,
MultiClumpCollide = 4096,
WeatherBrightness = 32768,
IsDamagable = 4096,
IsTree = 8192,
IsPalm = 16384,
DoesNotCollideWithFlyer = 32768,
ExplodeHit = 65536,
MultiClumpSpray = 1048576,
NoBackCull = 2097152
IsTag = 1048576,
NoBackCull = 2097152,
IsBreakableStatue = 4194304,
}
public interface ISimpleObjectDefinition : IObjectDefinition

View file

@ -12,7 +12,7 @@ namespace SanAndreasUnity.Importing.Items
private static readonly List<Zone> _zones = new List<Zone>();
private static readonly List<EntranceExit> _enexes = new List<EntranceExit>();
public static List<EntranceExit> Enexes => _enexes;
public static IReadOnlyList<EntranceExit> Enexes => _enexes;
private static readonly Dictionary<int, IObjectDefinition> _definitions
= new Dictionary<int, IObjectDefinition>();
@ -119,7 +119,7 @@ namespace SanAndreasUnity.Importing.Items
int lastCell = -1;
foreach (var inst in list)
{
int cell = inst.CellId & 0xff;
int cell = inst.InteriorLevel;
if (lastCell != cell && !_placements.ContainsKey(lastCell = cell))
{
_placements.Add(cell, new List<Placement>());

View file

@ -11,12 +11,12 @@ namespace SanAndreasUnity.Importing.Items.Placements
public class EntranceExit : Placement
{
public readonly UnityEngine.Vector3 EntrancePos;
public readonly float EntranceAngle;
public readonly UnityEngine.Vector3 EntrancePos; // position where enex is located
public readonly float EntranceAngle; // rotation of ped when entering enex
public readonly UnityEngine.Vector3 Size;
public readonly UnityEngine.Vector3 ExitPos;
public readonly float ExitAngle;
public readonly int TargetInterior;
public readonly UnityEngine.Vector3 ExitPos; // position of ped after teleporting to this enex
public readonly float ExitAngle; // rotation of ped after teleporting to this enex
public readonly int TargetInterior; // interior level where enex is located
public readonly int Flags;
public readonly string Name;
public readonly int SkyColorType;

View file

@ -24,6 +24,7 @@ namespace SanAndreasUnity.Importing.Items.Placements
public readonly int ObjectId;
public readonly string LodGeometry;
public readonly int CellId;
public int InteriorLevel => this.CellId & 0xff;
public readonly UnityEngine.Vector3 Position;
public readonly UnityEngine.Quaternion Rotation;
public readonly int LodIndex;

View file

@ -6,18 +6,9 @@ using SanAndreasUnity.Behaviours.Vehicles;
namespace SanAndreasUnity.Settings {
public class CameraSettings : MonoBehaviour {
static float s_farClipPlane = 1000;
static float s_fieldOfView = 60;
OptionsWindow.FloatInput m_farClipPlaneInput = new OptionsWindow.FloatInput() {
description = "Far clip plane",
minValue = 100,
maxValue = 5000,
getValue = () => Camera.main != null ? Camera.main.farClipPlane : s_farClipPlane,
setValue = (value) => { s_farClipPlane = value; if (Camera.main != null) Camera.main.farClipPlane = value; },
persistType = OptionsWindow.InputPersistType.OnStart,
};
OptionsWindow.FloatInput m_fieldOfViewInput = new OptionsWindow.FloatInput() {
description = "Field of view",
minValue = 20,
@ -47,7 +38,7 @@ namespace SanAndreasUnity.Settings {
void Awake ()
{
OptionsWindow.RegisterInputs ("CAMERA", m_farClipPlaneInput, m_fieldOfViewInput, m_cameraDistanceFromPed, m_cameraDistanceFromVehicle);
OptionsWindow.RegisterInputs ("CAMERA", m_fieldOfViewInput, m_cameraDistanceFromPed, m_cameraDistanceFromVehicle);
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += (s1, s2) => OnActiveSceneChanged();
}
@ -65,7 +56,6 @@ namespace SanAndreasUnity.Settings {
Camera cam = Utilities.F.FindMainCameraEvenIfDisabled();
if (cam != null)
{
cam.farClipPlane = s_farClipPlane;
cam.fieldOfView = s_fieldOfView;
}

View file

@ -1,41 +1,44 @@
using System.Collections.Generic;
using SanAndreasUnity.Behaviours.World;
using UnityEngine;
using SanAndreasUnity.Behaviours.World;
using SanAndreasUnity.UI;
namespace SanAndreasUnity.Settings {
namespace SanAndreasUnity.Settings
{
public class WorldSettings : MonoBehaviour
{
private static WorldSettings Singleton { get; set; }
public class WorldSettings : MonoBehaviour {
private float _drawDistanceToApply = 0f;
static float s_maxDrawDistance = 500f;
OptionsWindow.FloatInput m_maxDrawDistanceInput = new OptionsWindow.FloatInput() {
private OptionsWindow.FloatInput m_maxDrawDistanceInput = new OptionsWindow.FloatInput
{
description = "Max draw distance",
minValue = 50,
maxValue = 1000,
getValue = () => Cell.Instance != null ? Cell.Instance.maxDrawDistance : s_maxDrawDistance,
setValue = (value) => { s_maxDrawDistance = value; if (Cell.Instance != null) Cell.Instance.maxDrawDistance = value; },
persistType = OptionsWindow.InputPersistType.OnStart
minValue = WorldManager.MinMaxDrawDistance,
maxValue = WorldManager.MaxMaxDrawDistance,
getValue = () => WorldManager.Singleton.MaxDrawDistance,
setValue = value => { Singleton.OnDrawDistanceChanged(value); },
persistType = OptionsWindow.InputPersistType.OnStart,
};
void Awake ()
{
Singleton = this;
OptionsWindow.RegisterInputs ("WORLD", m_maxDrawDistanceInput);
UnityEngine.SceneManagement.SceneManager.activeSceneChanged += (s1, s2) => OnActiveSceneChanged();
}
void OnActiveSceneChanged()
void OnDrawDistanceChanged(float newValue)
{
// apply settings
// we need to find Cell with FindObjectOfType(), because it's Awake() method may have not been called yet
Cell cell = Object.FindObjectOfType<Cell>();
if (cell != null)
cell.maxDrawDistance = s_maxDrawDistance;
this.CancelInvoke(nameof(ChangeDrawDistanceDelayed));
_drawDistanceToApply = newValue;
this.Invoke(nameof(ChangeDrawDistanceDelayed), 0.2f);
}
void ChangeDrawDistanceDelayed()
{
WorldManager.Singleton.MaxDrawDistance = _drawDistanceToApply;
}
}
}

View file

@ -1,10 +1,15 @@
using System.Linq;
using SanAndreasUnity.Behaviours;
using SanAndreasUnity.Behaviours.World;
using UnityEngine;
namespace SanAndreasUnity.Stats
{
public class WorldStats : MonoBehaviour
{
private readonly System.Text.StringBuilder _stringBuilder = new System.Text.StringBuilder();
void Start()
{
Utilities.Stats.RegisterStat(new Utilities.Stats.Entry(){category = "WORLD", onGUI = OnStatGUI});
@ -12,16 +17,63 @@ namespace SanAndreasUnity.Stats
void OnStatGUI()
{
var sb = _stringBuilder;
sb.Clear();
if (Behaviours.World.Cell.Instance != null)
var cell = Cell.Instance;
if (cell != null)
{
Behaviours.World.Cell.Instance.showWindow (0);
}
sb.Append($"max draw distance {cell.MaxDrawDistance}\n");
sb.Append($"num focus points {cell.FocusPoints.Count}\n");
sb.Append($"num static objects {cell.NumStaticGeometries}\n");
sb.Append($"num TOBJ objects {StaticGeometry.TimedObjects.Count}\n");
sb.Append($"num active ENEX objects {EntranceExitMapObject.AllActiveObjects.Count}\n");
sb.Append($"num active objects with lights {StaticGeometry.ActiveObjectsWithLights.Count}\n");
sb.Append($"num active lights {StaticGeometry.ActiveObjectsWithLights.Sum(_ => _.NumLightSources)}\n");
sb.Append($"geometry parts loaded {Importing.Conversion.Geometry.NumGeometryPartsLoaded}\n");
sb.Append($"world systems\n");
for (int i = 0; i < cell.WorldSystem.WorldSystems.Count; i++)
{
sb.Append($"\tdistance level {cell.WorldSystem.DistanceLevels[i]}\n");
var worldSystem = cell.WorldSystem.WorldSystems[i];
sb.Append($"\tnum focus points {worldSystem.FocusPoints.Count}\n");
sb.Append($"\tnum areas {worldSystem.GetNumAreas(0)} {worldSystem.GetNumAreas(1)} {worldSystem.GetNumAreas(2)}\n");
if (Ped.Instance != null)
{
int numAreasVisible = 0,
numObjectsInVisibleAreas = 0,
maxNumFocusPointsThatSeeMe = 0,
minNumFocusPointsThatSeeMe = int.MaxValue;
worldSystem.ForEachAreaInRadius(
Ped.Instance.transform.position,
Mathf.Min(cell.WorldSystem.DistanceLevels[i], cell.MaxDrawDistance),
area =>
{
numAreasVisible++;
numObjectsInVisibleAreas += area.ObjectsInside?.Count ?? 0;
maxNumFocusPointsThatSeeMe = Mathf.Max(maxNumFocusPointsThatSeeMe, area.FocusPointsThatSeeMe?.Count ?? 0);
minNumFocusPointsThatSeeMe = Mathf.Min(minNumFocusPointsThatSeeMe, area.FocusPointsThatSeeMe?.Count ?? 0);
}
);
sb.Append($"\tlocal ped visibility\n");
sb.Append($"\t\tnum areas visible {numAreasVisible}\n");
sb.Append($"\t\tnum objects in visible areas {numObjectsInVisibleAreas}\n");
sb.Append($"\t\tmax num focus points that see an area {maxNumFocusPointsThatSeeMe}\n");
sb.Append($"\t\tmin num focus points that see an area {minNumFocusPointsThatSeeMe}\n");
}
sb.Append($"\n");
}
}
else
{
GUILayout.Label("World not loaded");
sb.Append($"World not loaded\n");
}
GUILayout.Label(sb.ToString());
}
}

View file

@ -605,9 +605,12 @@ namespace SanAndreasUnity.UI {
// draw enexes
if (m_drawEnexes)
{
foreach (var enex in Importing.Items.Item.Enexes.Where(enex => enex.TargetInterior == 0))
foreach (var enex in Importing.Items.Item.Enexes.Where(enex => Behaviours.World.Cell.IsExteriorLevel(enex.TargetInterior)))
{
this.DrawItemOnMap(MiniMap.Instance.GreenHouseTexture, enex.EntrancePos, 10);
this.DrawItemOnMap(
MiniMap.Instance.GreenHouseTexture,
Behaviours.World.Cell.Instance.GetPositionBasedOnInteriorLevel(enex.EntrancePos, enex.TargetInterior),
10);
}
}

View file

@ -63,11 +63,11 @@ namespace SanAndreasUnity.UI {
_spawnNames.Clear();
// if exterior is not loaded, then use enexes from loaded interiors
if (Cell.Instance != null && ! Cell.Instance.HasExterior)
if (Cell.Instance != null && ! Cell.Instance.HasMainExterior)
{
foreach(var enex in Cell.Instance.GetEnexesFromLoadedInteriors())
{
_spawns.Add(Cell.GetEnexExitTransform(enex));
_spawns.Add(Cell.Instance.GetEnexExitTransform(enex));
_spawnNames.Add(enex.Name);
}
}

View file

@ -7,7 +7,6 @@ namespace SanAndreasUnity.Utilities
public class AsyncLoader<TKey, TObj>
{
/// <summary>
/// All successfully loaded objects.
/// </summary>
@ -29,103 +28,84 @@ namespace SanAndreasUnity.Utilities
m_Loading = new Dictionary<TKey, List<System.Action<TObj>>> (comparer);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public int GetNumObjectsLoaded ()
{
return m_Loaded.Count;
}
[MethodImpl(MethodImplOptions.Synchronized)]
public int GetNumObjectsLoading ()
{
return m_Loading.Count;
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool IsObjectLoaded (TKey key)
{
return m_Loaded.ContainsKey (key);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public TObj GetLoadedObject (TKey key)
{
return m_Loaded [key];
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool CheckIsObjectLoaded (TKey key, System.Action<TObj> onFinish)
public bool TryLoadObject (TKey key, System.Action<TObj> onFinish)
{
ThreadHelper.ThrowIfNotOnMainThread(); // not needed, but to make sure
if (m_Loaded.ContainsKey(key))
{
onFinish (m_Loaded [key]);
return true;
onFinish(m_Loaded[key]);
return false;
}
return false;
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool CheckIsObjectLoading (TKey key, System.Action<TObj> onFinish)
{
if (m_Loading.ContainsKey (key))
if (m_Loading.ContainsKey(key))
{
// this object is loading
// subscribe to finish event
m_Loading[key].Add( onFinish );
return true;
m_Loading[key].Add(onFinish);
return false;
}
return false;
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool TryLoadObject (TKey key, System.Action<TObj> onFinish)
{
if (CheckIsObjectLoaded (key, onFinish))
return false;
if (CheckIsObjectLoading (key, onFinish))
return false;
// insert it into loading dict
m_Loading [key] = new List<System.Action<TObj>>(){onFinish};
m_Loading[key] = new List<System.Action<TObj>>() {onFinish};
return true;
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool TryGetLoadedObject(TKey key, out TObj loadedObject)
{
ThreadHelper.ThrowIfNotOnMainThread(); // not needed, but to make sure
return m_Loaded.TryGetValue(key, out loadedObject);
}
public void OnObjectFinishedLoading (TKey key, TObj obj, bool bSuccess)
{
ThreadHelper.ThrowIfNotOnMainThread(); // not needed, but to make sure
if (bSuccess)
{
if (m_Loaded.ContainsKey (key))
if (m_Loaded.ContainsKey(key))
{
// this object was loaded in the meantime
// this can happen if someone else is loading objects synchronously
Debug.LogErrorFormat ("Redundant load of object ({0}): {1}", typeof(TObj), key);
Debug.LogErrorFormat("Redundant load of object ({0}): {1}", typeof(TObj), key);
}
else
{
m_Loaded.Add (key, obj);
m_Loaded.Add(key, obj);
}
}
if (m_Loading.TryGetValue(key, out var subscribersList))
{
// remove from loading dict
m_Loading.Remove(key);
var list = m_Loading[key];
// remove from loading dict
m_Loading.Remove( key );
// invoke subscribers
foreach(var item in list)
Utilities.F.RunExceptionSafe( () => item(obj));
}
[MethodImpl(MethodImplOptions.Synchronized)]
public void AddToLoadedObjects (TKey key, TObj obj)
{
m_Loaded [key] = obj;
// invoke subscribers
foreach (var action in subscribersList)
Utilities.F.RunExceptionSafe(() => action(obj));
}
}
}

View file

@ -0,0 +1,87 @@
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
namespace SanAndreasUnity.Utilities
{
public class ConcurrentProducerConsumerSortedSet<T> : IProducerConsumerCollection<T>
{
private SortedSet<T> _sortedSet;
private object _lockObject = new object();
public ConcurrentProducerConsumerSortedSet(IComparer<T> comparer)
{
_sortedSet = new SortedSet<T>(comparer);
}
public IEnumerator<T> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public void CopyTo(Array array, int index)
{
throw new NotImplementedException();
}
public int Count
{
get
{
lock (_lockObject)
{
return _sortedSet.Count;
}
}
}
public bool IsSynchronized => throw new NotImplementedException();
public object SyncRoot => throw new NotImplementedException();
public void CopyTo(T[] array, int index)
{
throw new NotImplementedException();
}
public T[] ToArray()
{
throw new NotImplementedException();
}
public bool TryAdd(T item)
{
lock (_lockObject)
{
if (!_sortedSet.Add(item))
throw new ArgumentException($"Item with this key already exists: {item}");
return true;
}
}
public bool TryTake(out T result)
{
lock (_lockObject)
{
if (_sortedSet.Count > 0)
{
T min = _sortedSet.Min;
if (!_sortedSet.Remove(min))
throw new Exception("Failed to remove min element");
result = min;
return true;
}
result = default;
return false;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9c1410970028b404588b4f41a7f34497
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -13,49 +13,63 @@ namespace SanAndreasUnity.Utilities
/// </remarks>
public class ConcurrentQueue<T>
{
private readonly System.Object queueLock = new System.Object();
private readonly Queue<T> queue = new Queue<T>();
private readonly System.Object _queueLock = new System.Object();
private readonly Queue<T> _queue = new Queue<T>();
public void Enqueue(T item)
{
lock (queueLock)
lock (_queueLock)
{
queue.Enqueue(item);
_queue.Enqueue(item);
}
}
public bool TryDequeue(out T result)
{
lock (queueLock)
lock (_queueLock)
{
if (queue.Count == 0)
if (_queue.Count == 0)
{
result = default(T);
return false;
}
result = queue.Dequeue();
result = _queue.Dequeue();
return true;
}
}
public T[] DequeueAll()
{
lock (queueLock)
lock (_queueLock)
{
T[] copy = queue.ToArray();
queue.Clear();
T[] copy = _queue.ToArray();
_queue.Clear();
return copy;
}
}
public int DequeueToQueue(Queue<T> collection, int maxNumItems)
{
lock (_queueLock)
{
int numAdded = 0;
while (_queue.Count > 0 && numAdded < maxNumItems)
{
collection.Enqueue(_queue.Dequeue());
numAdded++;
}
return numAdded;
}
}
public int Count
{
get
{
lock (queueLock)
lock (_queueLock)
{
return queue.Count;
return _queue.Count;
}
}
}

View file

@ -0,0 +1,22 @@
using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace SanAndreasUnity.Utilities
{
public class DestroyWhenInHeadlessMode : MonoBehaviour
{
public Component[] componentsToDestroy = Array.Empty<Component>();
private void Start() // use Start() to avoid problems with script execution order
{
if (F.IsInHeadlessMode)
{
foreach (var component in this.componentsToDestroy)
{
Object.Destroy(component);
}
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4bb8f0fea23c3fe44828518a6ea9ad15
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,22 @@
using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace SanAndreasUnity.Utilities
{
public class DestroyWhenNotOnServer : MonoBehaviour
{
public Component[] componentsToDestroy = Array.Empty<Component>();
private void Awake()
{
if (!NetUtils.IsServer)
{
foreach (var component in this.componentsToDestroy)
{
Object.Destroy(component);
}
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d68c88ae66cbc64c997da0c710b080c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -408,6 +408,16 @@ namespace SanAndreasUnity.Utilities
return str[0].ToString().ToUpperInvariant() + str.Substring(1);
}
public static string ToLowerIfNotLower(this string str)
{
for (int i = 0; i < str.Length; i++)
{
if (!char.IsLower(str[i]))
return str.ToLower();
}
return str;
}
public static string GetGameObjectPath(this GameObject obj)
{
string path = obj.name;
@ -576,6 +586,16 @@ namespace SanAndreasUnity.Utilities
return enumerable.FindIndex (elem => elem.Equals (value));
}
public static bool AnyInList<T>(this IList<T> list, System.Predicate<T> predicate)
{
for (int i = 0; i < list.Count; i++)
{
if (predicate(list[i]))
return true;
}
return false;
}
public static bool AddIfNotPresent<T> (this List<T> list, T item)
{
if (!list.Contains (item)) {
@ -931,7 +951,17 @@ namespace SanAndreasUnity.Utilities
}
public static bool IsInHeadlessMode => SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null;
private static bool? _isInHeadlessModeCached;
public static bool IsInHeadlessMode
{
get
{
if (_isInHeadlessModeCached.HasValue)
return _isInHeadlessModeCached.Value;
_isInHeadlessModeCached = SystemInfo.graphicsDeviceType == UnityEngine.Rendering.GraphicsDeviceType.Null;
return _isInHeadlessModeCached.Value;
}
}
public static bool ScreenHasHighDensity => Application.isMobilePlatform;

View file

@ -11,10 +11,10 @@ namespace SanAndreasUnity.Utilities
var cam = Camera.current;
if (cam != null)
{
Vector3 f = -cam.transform.forward;
Quaternion quaternion = Quaternion.LookRotation(-cam.transform.forward, cam.transform.up);
for (int i = 0; i < this.transformsToFace.Length; i++)
{
this.transformsToFace[i].forward = f;
this.transformsToFace[i].rotation = quaternion;
}
}
}

View file

@ -1,46 +1,102 @@
using UnityEngine;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;
using SanAndreasUnity.Utilities;
using System.Threading;
using System.Runtime.CompilerServices;
using System.Collections.Concurrent;
using Debug = UnityEngine.Debug;
namespace SanAndreasUnity.Behaviours
{
public class LoadingThread : MonoBehaviour {
// TODO: maybe convert to class, because it takes 36 bytes - it's too much for red-black tree operations
public struct Job<T>
{
public System.Func<T> action ;
public System.Action<T> callbackSuccess ;
public System.Action<System.Exception> callbackError ;
public System.Action<T> callbackFinish;
public float priority;
internal object result ;
internal System.Exception exception ;
internal long id;
}
static System.Collections.Concurrent.BlockingCollection<Job<object>> s_jobs = new System.Collections.Concurrent.BlockingCollection<Job<object>> ();
static Thread s_thread;
static System.Collections.Concurrent.BlockingCollection<Job<object>> s_processedJobs = new System.Collections.Concurrent.BlockingCollection<Job<object>> ();
public class JobComparer : IComparer<Job<object>>
{
public int Compare(Job<object> a, Job<object> b)
{
if (a.id == b.id)
return 0;
static bool s_shouldThreadExit = false;
if (a.priority != b.priority)
return a.priority <= b.priority ? -1 : 1;
// priorities are the same
// the advantage has the job which was created earlier
return a.id <= b.id ? -1 : 1;
}
}
private class ThreadParameters
{
public readonly BlockingCollection<Job<object>> jobs =
new BlockingCollection<Job<object>> (new System.Collections.Concurrent.ConcurrentQueue<Job<object>>());
public readonly Utilities.ConcurrentQueue<Job<object>> processedJobs = new Utilities.ConcurrentQueue<Job<object>>();
private bool _shouldThreadExit = false;
private readonly object _shouldThreadExitLockObject = new object();
public bool ShouldThreadExit()
{
lock (_shouldThreadExitLockObject)
return _shouldThreadExit;
}
public void TellThreadToExit()
{
lock (_shouldThreadExitLockObject)
_shouldThreadExit = true;
}
}
public static LoadingThread Singleton { get; private set; }
private Thread _thread;
private readonly ThreadParameters _threadParameters = new ThreadParameters();
private readonly Queue<Job<object>> _processedJobsBuffer = new Queue<Job<object>>(256);
private static long s_lastJobId = 1;
private static readonly object s_lastJobIdLockObject = new object();
private readonly Stopwatch _stopwatch = new Stopwatch();
public ushort maxTimePerFrameMs = 0;
private void Awake()
{
Singleton = this;
}
void Start () {
s_thread = new Thread (ThreadFunction);
s_thread.Start ();
_thread = new Thread (ThreadFunction);
_thread.Start(_threadParameters);
}
void OnDisable ()
{
if (s_thread != null)
if (_thread != null)
{
var sw = System.Diagnostics.Stopwatch.StartNew ();
// s_thread.Interrupt ();
TellThreadToExit ();
if (s_thread.Join (7000))
// _thread.Interrupt ();
_threadParameters.TellThreadToExit ();
if (_thread.Join (7000))
Debug.LogFormat ("Stopped loading thread in {0} ms", sw.Elapsed.TotalMilliseconds);
else
Debug.LogError ("Failed to stop loading thread");
@ -51,9 +107,26 @@ namespace SanAndreasUnity.Behaviours
// get all processed jobs
_stopwatch.Restart();
Job<object> job;
while (s_processedJobs.TryTake (out job, 0))
while (true)
{
if (this.maxTimePerFrameMs != 0 && _stopwatch.ElapsedMilliseconds >= this.maxTimePerFrameMs)
break;
if (_processedJobsBuffer.Count > 0)
job = _processedJobsBuffer.Dequeue();
else
{
int numCopied = _threadParameters.processedJobs.DequeueToQueue(_processedJobsBuffer, 256);
if (numCopied == 0)
break;
job = _processedJobsBuffer.Dequeue();
}
if (job.exception != null)
{
// error happened
@ -80,12 +153,19 @@ namespace SanAndreasUnity.Behaviours
public static void RegisterJob<T> (Job<T> job)
{
// note: this function can be called from any thread
if (null == job.action)
return;
throw new ArgumentException("Job must have an action");
if (0f == job.priority)
throw new ArgumentException("You forgot to assign job priority");
job.exception = null;
var j = new Job<object> () {
id = GetNextJobId(),
priority = job.priority,
action = () => job.action(),
callbackError = job.callbackError,
};
@ -94,28 +174,25 @@ namespace SanAndreasUnity.Behaviours
if(job.callbackFinish != null)
j.callbackFinish = (arg) => job.callbackFinish( (T) arg );
s_jobs.Add (j);
Singleton._threadParameters.jobs.Add (j);
}
[MethodImpl(MethodImplOptions.Synchronized)]
static bool ShouldThreadExit()
static long GetNextJobId()
{
return s_shouldThreadExit;
lock (s_lastJobIdLockObject)
{
return s_lastJobId++;
}
}
[MethodImpl(MethodImplOptions.Synchronized)]
static void TellThreadToExit()
static void ThreadFunction (object objectParameter)
{
s_shouldThreadExit = true;
}
ThreadParameters threadParameters = (ThreadParameters) objectParameter;
static void ThreadFunction ()
{
while (!ShouldThreadExit ())
while (!threadParameters.ShouldThreadExit())
{
Job<object> job;
if (!s_jobs.TryTake (out job, 200))
if (!threadParameters.jobs.TryTake (out job, 200))
continue;
try
@ -127,7 +204,7 @@ namespace SanAndreasUnity.Behaviours
job.exception = ex;
}
s_processedJobs.Add (job);
threadParameters.processedJobs.Enqueue(job);
}
}

View file

@ -0,0 +1,14 @@
using System;
using System.Threading;
namespace SanAndreasUnity.Utilities
{
public static class ThreadHelper
{
public static void ThrowIfNotOnMainThread()
{
if (Thread.CurrentThread.ManagedThreadId != 1)
throw new Exception("This can only be executed on the main thread");
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ed180a98e16c5a4cbcc059ace2bb0ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -17,7 +17,7 @@ PhysicsManager:
m_ClothInterCollisionDistance: 0
m_ClothInterCollisionStiffness: 0
m_ContactsGeneration: 1
m_LayerCollisionMatrix: ffcfffffffcfffffffcfffffffffffffffcfffffffcfffffffffffffffffffffffcfffffff87ffffffcfffffff8dffffc8c0ffffc8c0ffffffb5ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
m_LayerCollisionMatrix: ff4fffffff4fffffff4fffffffffffffff4fffffff4fffffffffffffffffffffff4fffffff87ffffff4fffffff8dffffc840ffffc840ffffff35ffffc80affffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
m_AutoSimulation: 1
m_AutoSyncTransforms: 1
m_ReuseCollisionCallbacks: 0