forked from tmokmss/com.unity.multiplayer.samples.coop
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ServerCharacter.cs
403 lines (336 loc) · 14.3 KB
/
ServerCharacter.cs
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
using System;
using System.Collections;
using Unity.BossRoom.ConnectionManagement;
using Unity.BossRoom.Gameplay.Actions;
using Unity.BossRoom.Gameplay.Configuration;
using Unity.BossRoom.Gameplay.GameplayObjects.Character.AI;
using Unity.Multiplayer.Samples.BossRoom;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.Serialization;
using Action = Unity.BossRoom.Gameplay.Actions.Action;
namespace Unity.BossRoom.Gameplay.GameplayObjects.Character
{
/// <summary>
/// Contains all NetworkVariables, RPCs and server-side logic of a character.
/// This class was separated in two to keep client and server context self contained. This way you don't have to continuously ask yourself if code is running client or server side.
/// </summary>
[RequireComponent(typeof(NetworkHealthState),
typeof(NetworkLifeState),
typeof(NetworkAvatarGuidState))]
public class ServerCharacter : NetworkBehaviour, ITargetable
{
[FormerlySerializedAs("m_ClientVisualization")]
[SerializeField]
ClientCharacter m_ClientCharacter;
public ClientCharacter clientCharacter => m_ClientCharacter;
[SerializeField]
CharacterClass m_CharacterClass;
public CharacterClass CharacterClass
{
get
{
if (m_CharacterClass == null)
{
m_CharacterClass = m_State.RegisteredAvatar.CharacterClass;
}
return m_CharacterClass;
}
set => m_CharacterClass = value;
}
/// Indicates how the character's movement should be depicted.
public NetworkVariable<MovementStatus> MovementStatus { get; } = new NetworkVariable<MovementStatus>();
public NetworkVariable<ulong> HeldNetworkObject { get; } = new NetworkVariable<ulong>();
/// <summary>
/// Indicates whether this character is in "stealth mode" (invisible to monsters and other players).
/// </summary>
public NetworkVariable<bool> IsStealthy { get; } = new NetworkVariable<bool>();
public NetworkHealthState NetHealthState { get; private set; }
/// <summary>
/// The active target of this character.
/// </summary>
public NetworkVariable<ulong> TargetId { get; } = new NetworkVariable<ulong>();
/// <summary>
/// Current HP. This value is populated at startup time from CharacterClass data.
/// </summary>
public int HitPoints
{
get => NetHealthState.HitPoints.Value;
private set => NetHealthState.HitPoints.Value = value;
}
public NetworkLifeState NetLifeState { get; private set; }
/// <summary>
/// Current LifeState. Only Players should enter the FAINTED state.
/// </summary>
public LifeState LifeState
{
get => NetLifeState.LifeState.Value;
private set => NetLifeState.LifeState.Value = value;
}
/// <summary>
/// Returns true if this Character is an NPC.
/// </summary>
public bool IsNpc => CharacterClass.IsNpc;
public bool IsValidTarget => LifeState != LifeState.Dead;
/// <summary>
/// Returns true if the Character is currently in a state where it can play actions, false otherwise.
/// </summary>
public bool CanPerformActions => LifeState == LifeState.Alive;
/// <summary>
/// Character Type. This value is populated during character selection.
/// </summary>
public CharacterTypeEnum CharacterType => CharacterClass.CharacterType;
private ServerActionPlayer m_ServerActionPlayer;
/// <summary>
/// The Character's ActionPlayer. This is mainly exposed for use by other Actions. In particular, users are discouraged from
/// calling 'PlayAction' directly on this, as the ServerCharacter has certain game-level checks it performs in its own wrapper.
/// </summary>
public ServerActionPlayer ActionPlayer => m_ServerActionPlayer;
[SerializeField]
[Tooltip("If set to false, an NPC character will be denied its brain (won't attack or chase players)")]
private bool m_BrainEnabled = true;
[SerializeField]
[Tooltip("Setting negative value disables destroying object after it is killed.")]
private float m_KilledDestroyDelaySeconds = 3.0f;
[SerializeField]
[Tooltip("If set, the ServerCharacter will automatically play the StartingAction when it is created. ")]
private Action m_StartingAction;
[SerializeField]
DamageReceiver m_DamageReceiver;
[SerializeField]
ServerCharacterMovement m_Movement;
public ServerCharacterMovement Movement => m_Movement;
[SerializeField]
PhysicsWrapper m_PhysicsWrapper;
public PhysicsWrapper physicsWrapper => m_PhysicsWrapper;
[SerializeField]
ServerAnimationHandler m_ServerAnimationHandler;
public ServerAnimationHandler serverAnimationHandler => m_ServerAnimationHandler;
private AIBrain m_AIBrain;
NetworkAvatarGuidState m_State;
void Awake()
{
m_ServerActionPlayer = new ServerActionPlayer(this);
NetLifeState = GetComponent<NetworkLifeState>();
NetHealthState = GetComponent<NetworkHealthState>();
m_State = GetComponent<NetworkAvatarGuidState>();
}
public override void OnNetworkSpawn()
{
if (!IsServer) { enabled = false; }
else
{
NetLifeState.LifeState.OnValueChanged += OnLifeStateChanged;
m_DamageReceiver.DamageReceived += ReceiveHP;
m_DamageReceiver.CollisionEntered += CollisionEntered;
if (IsNpc)
{
m_AIBrain = new AIBrain(this, m_ServerActionPlayer);
}
if (m_StartingAction != null)
{
var startingAction = new ActionRequestData() { ActionID = m_StartingAction.ActionID };
PlayAction(ref startingAction);
}
InitializeHitPoints();
}
}
public override void OnNetworkDespawn()
{
NetLifeState.LifeState.OnValueChanged -= OnLifeStateChanged;
if (m_DamageReceiver)
{
m_DamageReceiver.DamageReceived -= ReceiveHP;
m_DamageReceiver.CollisionEntered -= CollisionEntered;
}
}
/// <summary>
/// RPC to send inputs for this character from a client to a server.
/// </summary>
/// <param name="movementTarget">The position which this character should move towards.</param>
[ServerRpc]
public void SendCharacterInputServerRpc(Vector3 movementTarget)
{
if (LifeState == LifeState.Alive && !m_Movement.IsPerformingForcedMovement())
{
// if we're currently playing an interruptible action, interrupt it!
if (m_ServerActionPlayer.GetActiveActionInfo(out ActionRequestData data))
{
if (GameDataSource.Instance.GetActionPrototypeByID(data.ActionID).Config.ActionInterruptible)
{
m_ServerActionPlayer.ClearActions(false);
}
}
m_ServerActionPlayer.CancelRunningActionsByLogic(ActionLogic.Target, true); //clear target on move.
m_Movement.SetMovementTarget(movementTarget);
}
}
// ACTION SYSTEM
/// <summary>
/// Client->Server RPC that sends a request to play an action.
/// </summary>
/// <param name="data">Data about which action to play and its associated details. </param>
[ServerRpc]
public void RecvDoActionServerRPC(ActionRequestData data)
{
ActionRequestData data1 = data;
if (!GameDataSource.Instance.GetActionPrototypeByID(data1.ActionID).Config.IsFriendly)
{
// notify running actions that we're using a new attack. (e.g. so Stealth can cancel itself)
ActionPlayer.OnGameplayActivity(Action.GameplayActivity.UsingAttackAction);
}
PlayAction(ref data1);
}
// UTILITY AND SPECIAL-PURPOSE RPCs
/// <summary>
/// Called on server when the character's client decides they have stopped "charging up" an attack.
/// </summary>
[ServerRpc]
public void RecvStopChargingUpServerRpc()
{
m_ServerActionPlayer.OnGameplayActivity(Action.GameplayActivity.StoppedChargingUp);
}
void InitializeHitPoints()
{
HitPoints = CharacterClass.BaseHP.Value;
if (!IsNpc)
{
SessionPlayerData? sessionPlayerData = SessionManager<SessionPlayerData>.Instance.GetPlayerData(OwnerClientId);
if (sessionPlayerData is { HasCharacterSpawned: true })
{
HitPoints = sessionPlayerData.Value.CurrentHitPoints;
if (HitPoints <= 0)
{
LifeState = LifeState.Fainted;
}
}
}
}
/// <summary>
/// Play a sequence of actions!
/// </summary>
public void PlayAction(ref ActionRequestData action)
{
//the character needs to be alive in order to be able to play actions
if (LifeState == LifeState.Alive && !m_Movement.IsPerformingForcedMovement())
{
if (action.CancelMovement)
{
m_Movement.CancelMove();
}
m_ServerActionPlayer.PlayAction(ref action);
}
}
void OnLifeStateChanged(LifeState prevLifeState, LifeState lifeState)
{
if (lifeState != LifeState.Alive)
{
m_ServerActionPlayer.ClearActions(true);
m_Movement.CancelMove();
}
}
IEnumerator KilledDestroyProcess()
{
yield return new WaitForSeconds(m_KilledDestroyDelaySeconds);
if (NetworkObject != null)
{
NetworkObject.Despawn(true);
}
}
/// <summary>
/// Receive an HP change from somewhere. Could be healing or damage.
/// </summary>
/// <param name="inflicter">Person dishing out this damage/healing. Can be null. </param>
/// <param name="HP">The HP to receive. Positive value is healing. Negative is damage. </param>
void ReceiveHP(ServerCharacter inflicter, int HP)
{
//to our own effects, and modify the damage or healing as appropriate. But in this game, we just take it straight.
if (HP > 0)
{
m_ServerActionPlayer.OnGameplayActivity(Action.GameplayActivity.Healed);
float healingMod = m_ServerActionPlayer.GetBuffedValue(Action.BuffableValue.PercentHealingReceived);
HP = (int)(HP * healingMod);
}
else
{
#if UNITY_EDITOR || DEVELOPMENT_BUILD
// Don't apply damage if god mode is on
if (NetLifeState.IsGodMode.Value)
{
return;
}
#endif
m_ServerActionPlayer.OnGameplayActivity(Action.GameplayActivity.AttackedByEnemy);
float damageMod = m_ServerActionPlayer.GetBuffedValue(Action.BuffableValue.PercentDamageReceived);
HP = (int)(HP * damageMod);
serverAnimationHandler.NetworkAnimator.SetTrigger("HitReact1");
}
HitPoints = Mathf.Clamp(HitPoints + HP, 0, CharacterClass.BaseHP.Value);
if (m_AIBrain != null)
{
//let the brain know about the modified amount of damage we received.
m_AIBrain.ReceiveHP(inflicter, HP);
}
//we can't currently heal a dead character back to Alive state.
//that's handled by a separate function.
if (HitPoints <= 0)
{
if (IsNpc)
{
if (m_KilledDestroyDelaySeconds >= 0.0f && LifeState != LifeState.Dead)
{
StartCoroutine(KilledDestroyProcess());
}
LifeState = LifeState.Dead;
}
else
{
LifeState = LifeState.Fainted;
}
m_ServerActionPlayer.ClearActions(false);
}
}
/// <summary>
/// Determines a gameplay variable for this character. The value is determined
/// by the character's active Actions.
/// </summary>
/// <param name="buffType"></param>
/// <returns></returns>
public float GetBuffedValue(Action.BuffableValue buffType)
{
return m_ServerActionPlayer.GetBuffedValue(buffType);
}
/// <summary>
/// Receive a Life State change that brings Fainted characters back to Alive state.
/// </summary>
/// <param name="inflicter">Person reviving the character.</param>
/// <param name="HP">The HP to set to a newly revived character.</param>
public void Revive(ServerCharacter inflicter, int HP)
{
if (LifeState == LifeState.Fainted)
{
HitPoints = Mathf.Clamp(HP, 0, CharacterClass.BaseHP.Value);
NetLifeState.LifeState.Value = LifeState.Alive;
}
}
void Update()
{
m_ServerActionPlayer.OnUpdate();
if (m_AIBrain != null && LifeState == LifeState.Alive && m_BrainEnabled)
{
m_AIBrain.Update();
}
}
void CollisionEntered(Collision collision)
{
if (m_ServerActionPlayer != null)
{
m_ServerActionPlayer.CollisionEntered(collision);
}
}
/// <summary>
/// This character's AIBrain. Will be null if this is not an NPC.
/// </summary>
public AIBrain AIBrain { get { return m_AIBrain; } }
}
}