-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInstance.cs
More file actions
277 lines (250 loc) · 13.2 KB
/
Instance.cs
File metadata and controls
277 lines (250 loc) · 13.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// Instance for instanced dungeons, etc.
using System.Collections.Generic;
using UnityEngine;
using Mirror;
using Unity.AI.Navigation;
namespace uMMORPG
{
// requires NavMeshSurface so that we can duplicate and move it at runtime.
[RequireComponent(typeof(NavMeshSurface))]
public class Instance : MonoBehaviour
{
[Header("Instance Definition")]
public Transform entry;
public int requiredLevel = 1;
[HideInInspector] public Bounds bounds;
NavMeshSurface navMeshSurface;
[Tooltip("Only allow so many instances of this type to protect server resources.")]
public int instanceLimit = 5;
// all the instances that are based on this template.
// dict <partyId, Instance>
[HideInInspector] public Dictionary<int, Instance> instances = new Dictionary<int, Instance>();
[Header("Party Check")]
public LayerMask playerLayers = ~0; // Everything by default. Make sure to select 'Player' layer.
public float partyCheckInterval = 30;
double nextPartyCheckTime;
bool localPlayerEnteredYet;
// cache SpawnPoints so we don't have to call GetComponentsInChildren for
// each new instance. automatically assigned in OnValidate!
// => we can't keep scene monsters in there when instantiating because we
// would have duplicate sceneIds/netIds, which would get everything out
// of sync.
// (force overwriting sceneId to 0 can be done, but the duplicates still
// have 0 netIds. either way it's too hacky.)
// => this way we also don't waste server resources because we don't create
// hundreds of monsters per instance template on start. only when needed.
[Header("Spawn Points Cache")]
public InstanceSpawnPoint[] spawnPoints;
// reference to original instance template that this instance was created
// from. useful to remove ourselves from the template's instance list later.
Instance template;
// the party that owns this instance. 0 means none.
int partyId = 0;
// OverlapBoxNonAlloc array to avoid allocations.
// -> static so we don't create one per skill
// -> this is worth it because skills are casted a lot!
// -> should be big enough to work in just about all cases
static Collider[] hitsBuffer = new Collider[10000];
void Awake()
{
bounds = Utils.CalculateBoundsForAllRenderers(gameObject);
// Disable NavMesh building on clients
if (!NetworkServer.active && navMeshSurface != null)
{
navMeshSurface.enabled = false;
}
}
void OnValidate()
{
// no NetworkServer.active check here because it needs to work in Editor.
// cache spawnpoints
spawnPoints = GetComponentsInChildren<InstanceSpawnPoint>();
// make sure that no child object is marked as 'static'. static objects
// can't be duplicated and moved, which is what we need for instances.
// (otherwise moved meshes might appear empty because they are still on
// the old position (static)).
foreach (Transform tf in GetComponentsInChildren<Transform>())
if (tf.gameObject.isStatic)
Debug.LogWarning("Instance child " + tf.name + " shouldn't be static. It needs to be duplicated and moved to other positions when duplicating instances.");
}
HashSet<Player> FindAllPlayersInInstanceBounds()
{
// HashSet so we don't accidentally add a player twice in case the cast
// detected two of his colliders.
HashSet<Player> result = new HashSet<Player>();
int hits = Physics.OverlapBoxNonAlloc(bounds.center, bounds.extents, hitsBuffer, transform.rotation, playerLayers);
for (int i = 0; i < hits; ++i)
{
Collider co = hitsBuffer[i];
Player player = co.GetComponentInParent<Player>();
if (player != null)
result.Add(player);
}
return result;
}
void DestroyAllNetworkIdentitiesInInstanceBounds()
{
int hits = Physics.OverlapBoxNonAlloc(bounds.center, bounds.extents, hitsBuffer, transform.rotation);
for (int i = 0; i < hits; ++i)
{
Collider co = hitsBuffer[i];
NetworkIdentity identity = co.GetComponentInParent<NetworkIdentity>();
if (identity != null)
NetworkServer.Destroy(identity.gameObject);
}
}
void Update()
{
// run destroy checks if this is an instance with a valid party id
// (not a template that lives in the scene)
if (partyId > 0)
{
// server checks containing members in an interval
if (NetworkServer.active)
{
// interval elapsed?
if (NetworkTime.time >= nextPartyCheckTime)
{
// find all players within instance bounds.
// players might be in this instance for all kinds of right and
// wrong reasons:
// * might be in the instance party
// * might have left the instance party
// * might have relogged and accidentally spawned in another party's
// instance
HashSet<Player> playersInInstanceBounds = FindAllPlayersInInstanceBounds();
int playersRemaining = 0;
foreach (Player player in playersInInstanceBounds)
{
// in party for this instance? then count
if (player.party.party.partyId == partyId)
{
++playersRemaining;
}
// otherwise kick from instance
else
{
Transform spawn = ((NetworkManagerMMO)NetworkManager.singleton).GetStartPositionFor(player.className);
player.movement.Warp(spawn.position);
Debug.Log("Removed player " + player.name + " with partyId=" + player.party.party.partyId + " from instance " + name + " with partyId=" + partyId);
}
}
// is no one is left then destroy the instance
if (playersRemaining == 0)
{
// destroy self
// TODO when does the client destroy it?
// TODO call networkserver.destroy for containing monsters, or not?
// TODO the monsters remain. either parent them when spawning, or destroy manually here.
Destroy(gameObject);
Debug.Log("Instance " + name + " destroyed because no members of party " + partyId + " are in it anymore.");
}
// reset interval
nextPartyCheckTime = NetworkTime.time + partyCheckInterval;
}
}
// client creates instance when entering and destroys it when leaving.
// simple as that.
// => we need to wait until the player entered the instance at least
// once though. otherwise we might destroy it immediately while
// the server's agent.warp packet is still being delivered to the
// client.
else if (Player.localPlayer != null)
{
// has the player entered the instance yet?
if (bounds.Contains(Player.localPlayer.transform.position))
{
localPlayerEnteredYet = true;
}
// not in instance anymore, but entered before?
else if (localPlayerEnteredYet)
{
Destroy(gameObject);
Debug.Log("Instance " + name + " destroyed for local player because he left the instance.");
}
}
}
}
void OnDestroy()
{
// no NetworkServer.active check here because we want to remove it from
// the instance list on client too!
// is this an instance of a template? then remove from template's list
// to free limits
if (template != null)
template.instances.Remove(partyId);
// if server then despawn all networkidentities properly
// -> there could be initially spawned monsters
// -> there could be additional spawns like monster scroll spawns
// => simply despawn any NetworkIdentity in bounds to be sure!
if (NetworkServer.active)
DestroyAllNetworkIdentitiesInInstanceBounds();
}
// create a new instance for a party
// -> this has to be called on the server AND on the client.
// they both instantiate based on a template because we can't
// NetworkServer.Spawn the instance (it might contain monsters, and a
// NetworkIdentity can't be a child of another NetworkIdentity).
public static Instance CreateInstance(Instance template, int partyId)
{
// instance for this party not created yet?
if (!template.instances.ContainsKey(partyId))
{
// instance limit for this template not reached yet?
if (template.instances.Count < template.instanceLimit)
{
// z-offset:
// -> bounds.size works perfectly fine
// -> + visRange so we don't waste bandwidth if two dungeons are so
// close that the proximity checkers would broadcast to both.
// -> * partyId so they aren't inside each other. using partyId
// makes sure that the client knows where to instantiate it
// too. if we were to use template.instances.Count then
// the client would have to know about that value with a
// custom NetworkMessages. this would be very complicated.
// => using partyId guarantees that no one can ever log back
// into the game and spawn in another party's dungeon that
// is currently where he logged out before
// => if floating point precision becomes an issue then we
// can still recycle party ids in PartySystem.cs!
float zOffset = (template.bounds.size.z + ((SpatialHashingInterestManagement)NetworkServer.aoi).visRange) * partyId;
Debug.Log("Creating " + template.name + " Instance with zOffset=" + zOffset);
// instantiate it
Vector3 position = template.transform.position + new Vector3(0, 0, zOffset);
GameObject go = Instantiate(template.gameObject, position, template.transform.rotation);
Instance instance = go.GetComponent<Instance>();
instance.template = template;
if (NetworkServer.active)
{
NavMeshSurface surface = instance.GetComponent<NavMeshSurface>();
if (surface != null)
surface.BuildNavMesh();
}
// set party id
// note: Update will start box casting immediately, but there is
// no race condition because the portal that calls
// CreateInstance will move a player into it immediately.
// (before next Update is called)
instance.partyId = partyId;
// add to list of the template's instances
template.instances[partyId] = instance;
// server spawns NetworkIdentities at spawn points
if (NetworkServer.active && instance.spawnPoints != null)
{
// spawn each of them
foreach (InstanceSpawnPoint spawnPoint in instance.spawnPoints)
{
GameObject spawned = Instantiate(spawnPoint.prefab.gameObject, spawnPoint.transform.position, spawnPoint.transform.rotation);
spawned.name = spawnPoint.prefab.name; // avoid "(Clone)"
NetworkServer.Spawn(spawned);
}
}
// return the instance
return instance;
}
}
else Debug.LogWarning("Instance " + template.name + " was already created for partyId=" + partyId + ". This should never happen.");
return null;
}
}
}