Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- using UnityEngine;
- using TNet;
- /// <summary>
- /// Race track checkpoint. Has all the functionality needed to have a functional race.
- /// </summary>
- [RequireComponent(typeof(OutpostObject))]
- public class Checkpoint : TNBehaviour
- {
- // Using 'Tooltip' attribute adds hover-over tooltips in Unity
- [Tooltip("Renderer with the main texture that will be getting replaced")]
- public Renderer flagRenderer;
- [Tooltip("Used when this is not the next checkpoint")]
- public Texture2D plainTexture;
- [Tooltip("Used to mark the next checkpoint that the player must reach")]
- public Texture2D checkpointTexture;
- static List<Checkpoint> mList = new List<Checkpoint>();
- static List<Checkpoint> mRaces = new List<Checkpoint>();
- /// <summary>
- /// Any class with non-reference members can be sent over the network. The 'vehicle' field is a reference, so it can't be sent.
- /// Instead, a 'vehicleID' identifier can be sent, allowing us to recover the 'vehicle' reference as needed.
- /// </summary>
- [System.Serializable]
- class Entry
- {
- [System.NonSerialized] public Vehicle vehicle;
- public ulong vehicleID;
- public int lastCP = 0;
- public int laps = 0;
- public long start = 0;
- public long lapStart = 0;
- public float bestLap = float.MaxValue;
- public float totalTime = 0f;
- public bool isFinished = false;
- }
- List<Entry> mRacers = new List<Entry>();
- OutpostObject mObj;
- Checkpoint mStart;
- Checkpoint mEnd;
- Checkpoint mPrev;
- Checkpoint mNext;
- Light mLight;
- bool mHighlighted = false;
- /// <summary>
- /// [GameOption] attribute makes fields show up when the object is right-clicked in the game.
- /// [Validator] attribute specifies a boolean-returning property that will be checked that determines whether the field will be shown.
- /// [GameTooltip] attribute adds a localization string keyword that will be shown when hovering over the field in the game.
- /// Note that the property name (in this case 'track') must also be a localized string in the Localization.csv file.
- /// </summary>
- [SkipRename, GameOption, Validator("canEdit"), GameTooltip("raceTrackInfo")]
- public int track { get { return Get<int>("track"); } set { if (value != track && value > -1) Set("track", value); } }
- [SkipRename, GameOption, Validator("canEdit"), GameTooltip("raceIndexInfo")]
- public int index { get { return Get<int>("index"); } set { if (value != index && value > -1) Set("index", value); } }
- [SkipRename, GameOption, Validator("canStart"), GameTooltip("raceLapsInfo")]
- public int laps { get { return (mStart != null && mStart != this) ? mStart.laps : Get("laps", 1); } set { if (value != index && value > 0) Set("laps", value); } }
- [SkipRename] bool isInteractable { get { return mObj != null && mObj.isInteractable; } }
- [SkipRename] bool canEdit { get { return isInteractable && !raceInProgress; } }
- [SkipRename] bool isStart { get { return index == 0 && Find(track, index) == this; } }
- [SkipRename] bool canStart { get { return isInteractable && isStart && !raceInProgress; } }
- [SkipRename] bool canEnd { get { return isInteractable && raceInProgress; } }
- [SkipRename] bool raceInProgress { get { return (mStart != null && mStart != this) ? mStart.raceInProgress : Get<bool>("active"); } }
- /// <summary>
- /// OnStart function is the best place to cache references to required components and subscribe to events.
- /// </summary>
- public override void OnStart ()
- {
- base.OnStart();
- mObj = GetComponent<OutpostObject>();
- mLight = GetComponentInChildren<Light>();
- if (mLight != null) mLight.enabled = false;
- flagRenderer.AddOnRender(OnRender);
- tno.onDataChanged += OnSetData;
- }
- /// <summary>
- /// When the object's data changes, relink and update the highlight state of all the checkpoints.
- /// </summary>
- void OnSetData (DataNode data)
- {
- foreach (var cp in mList) if (cp.isStart) cp.Link();
- if (mStart != null) mStart.UpdateHighlight();
- }
- /// <summary>
- /// We want to change the flag's texture based on the current state. To do this, we register a render callback on the renderer (in OnStart above).
- /// This function will be called when the renderer is being drawn by the game, allowing us to change all of its material properties as needed.
- /// </summary>
- void OnRender (MaterialPropertyBlock block)
- {
- if (mHighlighted) block.SetTexture("_MainTex", checkpointTexture);
- else block.SetTexture("_MainTex", plainTexture);
- }
- // Simple means of keeping track of all the game's active checkpoints is to add them to a list in OnEnable and remove in OnDisable.
- void OnEnable () { mList.Add(this); }
- void OnDisable () { mList.Remove(this); }
- /// <summary>
- /// Run through the list of checkpoints and find next and previous references for all of them belonging to the same race track as the 'start'.
- /// </summary>
- bool Link (Checkpoint start = null)
- {
- if (start == null) start = this;
- var end = start;
- for (int i = 0; i < mList.size; ++i)
- {
- var cp = mList.buffer[i];
- if (cp.track != track) continue;
- cp.mStart = start;
- cp.mPrev = cp.isStart ? null : cp.FindPrevious();
- cp.mNext = cp.FindNext();
- if (cp.mNext != null && end.index < cp.mNext.index) end = cp.mNext;
- }
- for (int i = 0; i < mList.size; ++i)
- {
- var cp = mList.buffer[i];
- if (cp.track != track) continue;
- cp.mEnd = end;
- }
- return end != start;
- }
- Checkpoint FindNext ()
- {
- Checkpoint best = null;
- foreach (var c in mList)
- {
- if (c.track != track) continue;
- if (c == this) continue;
- if (c.index <= index) continue;
- if (best == null || best.index > c.index) best = c;
- }
- return best;
- }
- Checkpoint FindPrevious ()
- {
- Checkpoint best = null;
- foreach (var c in mList)
- {
- if (c.track != track) continue;
- if (c == this) continue;
- if (c.index >= index) continue;
- if (best == null || best.index < c.index) best = c;
- }
- return best;
- }
- /// <summary>
- /// [GameOption] attribute can also be used on functions to add clickable buttons. In this case, this button lets us start the race in the first place.
- /// </summary>
- [SkipRename, GameOption, Validator("canStart")]
- void StartRace ()
- {
- if (Link())
- {
- // Set and Get functions should be used for anything that requires persistence, but is not frequent enough to warrant a custom RFC (remote function call).
- Set("active", true);
- // It's possible to create custom RFCs like this one to send out notifications to other players with any number of parameters (in this case none).
- // If the RFC is sent with a Target.AllSaved or Target.OthersSaved flag, it will be persistent (saved on the server). In this case, it's a simple
- // one-time notification for all players.
- tno.Send("OnStartRace", Target.All);
- }
- else
- {
- // UIStatusBar lets us show an alert-like notification at the top of the screen. There are also UIStatusBar.Show(), ShowLocalized(), and ShowError().
- UIStatusBar.ShowLocalizedError("No checkpoints", 3f);
- }
- }
- [RFC]
- void OnStartRace ()
- {
- if (Link())
- {
- // This is the player's directly controlled vehicle. ControllableEntity.mine would be the vehicle the player is in (but is not necessarily controlling).
- var vehicle = ControllableEntity.controlled;
- if (vehicle.GetDistanceTo(transform.position) < 40f)
- {
- // All FactionObject-derived classes can print text above them via AddHUDText. If you want all players to see this text, use SendHUDText instead.
- vehicle.AddHUDText(Localization.Get("GO"), Color.green, 4f);
- GameAudio.Play(GameAudio.instance.boom);
- }
- }
- }
- /// <summary>
- /// Button that lets us manually end the race. Unlike the StartRace function, this one's [Validator] attribute doesn't prevent it from being visible only on
- /// the starting checkpoint (index of 0). This makes it possible to end the race from any checkpoint.
- /// </summary>
- [SkipRename, GameOption, Validator("canEnd")]
- void EndRace ()
- {
- if (mStart != null && mStart != this)
- {
- // We want to only end the race on the same object that can start it: the start checkpoint.
- mStart.EndRace();
- }
- else
- {
- // Notify everyone that the race is ending
- tno.Send("OnEndRace", Target.All);
- // This actually ends the race by flipping the 'active' flag
- Set("active", false);
- // Clear all references to be on the safe side
- foreach (var cp in mList)
- {
- if (cp.mStart == this)
- {
- cp.mStart = null;
- cp.mEnd = null;
- cp.mNext = null;
- cp.mPrev = null;
- }
- }
- }
- }
- [RFC]
- void OnEndRace ()
- {
- var vehicle = ControllableEntity.controlled as Vehicle;
- if (vehicle != null && (FindRacer(vehicle) != null || vehicle.GetDistanceTo(transform.position) < 40f ||
- (mEnd != null && vehicle.GetDistanceTo(mEnd.transform.position) < 40f)))
- {
- // You can print text directly to chat
- UIGameChat.AddCurrent(Localization.Get("endRace"), Color.yellow);
- // By default, printing text to chat won't make the window show up if it was minimized. Calling Show() will force it to maximize.
- UIGameChat.Show();
- // Sort the list of racers in an ascending order (the sorting function is below)
- mRacers.Sort(SortByTime);
- // Print all racers to the chat window as a summary
- for (int i = 0, index = 1; i < mRacers.size; ++i)
- {
- var racer = mRacers[i];
- if (racer.totalTime == 0f || racer.vehicle == null) continue;
- UIGameChat.AddCurrent(Localization.Format("raceSummary", index, racer.vehicle.visibleName,
- GameTools.HighlightText(GameTools.FormatTimeMS(racer.totalTime)),
- GameTools.FormatTimeMS(racer.bestLap)), Color.yellow);
- ++index;
- }
- mRaces.Remove(this);
- mRacers.Clear();
- }
- }
- static int SortByTime (Entry a, Entry b)
- {
- var fa = (a.totalTime == 0f) ? float.MaxValue : a.totalTime;
- var fb = (b.totalTime == 0f) ? float.MaxValue : b.totalTime;
- return fa.CompareTo(fb);
- }
- static Checkpoint Find (int track, int index)
- {
- foreach (var cp in mList) if (cp.track == track && cp.index == index) return cp;
- return null;
- }
- Entry FindRacer (Vehicle v)
- {
- if (v == null) return null;
- foreach (var ent in mRacers) if (ent.vehicle == v) return ent;
- return null;
- }
- /// <summary>
- /// OnTriggerEnter is called whenever any object enter one of the trigger-marked colliders belonging to this script's rigidbody.
- /// Note that this will only work if this script is attached to the same game object as the rigidbody or the trigger collider itself.
- /// </summary>
- void OnTriggerEnter (Collider other)
- {
- // Don't even try to do anything unless a race is actually in progress
- if (!raceInProgress) return;
- // Only the controlling player should be performing checks
- var vehicle = other.GetComponentInParent<Vehicle>();
- if (vehicle != null && vehicle.isControlledByMe) Advance(vehicle, this);
- }
- /// <summary>
- /// Given the vehicle and the checkpoint it's passing through, progress the race.
- /// </summary>
- void Advance (Vehicle vehicle, Checkpoint currentCP)
- {
- if (mStart != null && mStart != this) { mStart.Advance(vehicle, currentCP); return; }
- if (!raceInProgress || vehicle.mainPlayer == null) return;
- var ent = FindRacer(vehicle);
- // Just in case -- this can trigger if reconnecting while a race is active
- if (mStart == null || mEnd == null) { if (!Link()) return; }
- if (ent == null)
- {
- // New racers must begin with the first checkpoint
- if (!currentCP.isStart) return;
- ent = new Entry();
- ent.vehicle = vehicle;
- ent.vehicleID = vehicle.uid;
- ent.start = TNManager.serverTime;
- ent.lapStart = ent.start;
- ent.lastCP = 0;
- mRacers.Add(ent);
- OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
- return;
- }
- // Wrong checkpoint
- if (ent.isFinished || ent.lastCP == currentCP.index) return;
- // Reached the end
- if (currentCP.index == mEnd.index)
- {
- if (laps < 2)
- {
- // Single lap race -- end point is the end
- ent.bestLap = (TNManager.serverTime - ent.start) * 0.001f;
- ent.totalTime = ent.bestLap;
- ent.isFinished = true;
- var s = Localization.Format("bestTime", GameTools.HighlightText(GameTools.FormatTimeMS(ent.bestLap)));
- UIGameChat.AddCurrent(s, Color.yellow);
- UIStatusBar.Show(s, 3f);
- OnAdvance(vehicle, ent, GameAudio.instance.success);
- }
- else
- {
- // It's a multi-lap race, the end checkpoint simply wraps around to the beginning
- ent.lastCP = mEnd.index;
- OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
- }
- }
- else if (currentCP.index == mStart.index && ent.lastCP == mEnd.index)
- {
- var time = TNManager.serverTime;
- var lapTime = (time - ent.lapStart) * 0.001f;
- ent.totalTime += lapTime;
- if (ent.bestLap > lapTime) ent.bestLap = lapTime;
- // Lap complete
- if (++ent.laps < laps)
- {
- // Next lap
- ent.lapStart = time;
- ent.lastCP = mStart.index;
- UIStatusBar.Show(Localization.Format("lapTime", ent.laps, GameTools.HighlightText(GameTools.FormatTimeMS(lapTime))), 3f);
- OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
- }
- else
- {
- // The race is over for this vehicle
- ent.isFinished = true;
- UIStatusBar.Show(Localization.Format("bestTime", GameTools.HighlightText(GameTools.FormatTimeMS(ent.bestLap))), 3f);
- OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
- }
- }
- else if (currentCP.mPrev != null && currentCP.mPrev.index == ent.lastCP)
- {
- ent.lastCP = currentCP.index;
- OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
- }
- }
- void OnAdvance (Vehicle vehicle, Entry ent, AudioClip audio)
- {
- UpdateHighlight(ent);
- // You can play sounds in 2D via GameAudio.Play, or in 3D via ControllableEntity.PlaySound.
- vehicle.PlaySound(audio);
- // Here we send out a packet to other players, passing the entire Entity class instance. Since this function is only ultimately
- // called by each vehicle's controlling player due to the isControlledByMe check in OnTriggerEnter, this means that each player
- // is responsible for keeing their own record up-to-date and informing everyone else.
- tno.Send("SyncPlayer", Target.Others, ent);
- // Check to see if the race should now be over
- CheckIfOver();
- }
- [RFC]
- void SyncPlayer (Entry ent)
- {
- // Controllable entities can be vehicles (land / air / sea), and turrets. Since turrets can't move, we only want vehicles here.
- // The soft C# cast (as Something) will simply be 'null' if it can't be converted.
- ent.vehicle = ControllableEntity.Find(ent.vehicleID) as Vehicle;
- for (int i = 0; i < mRacers.size; ++i)
- {
- var r = mRacers[i];
- if (r.vehicleID == ent.vehicleID)
- {
- mRacers[i] = ent;
- CheckIfOver();
- return;
- }
- }
- mRacers.Add(ent);
- }
- void UpdateHighlight ()
- {
- UpdateHighlight(raceInProgress ? FindRacer(ControllableEntity.controlled as Vehicle) : null);
- }
- /// <summary>
- /// This function determines whether the checkpoint should be highlighted or not. This is what makes the next checkpoint the player must pass through highlighted.
- /// </summary>
- void UpdateHighlight (Entry ent)
- {
- if (ent == null)
- {
- foreach (var cp in mList)
- {
- if (cp.track == track) cp.SetHighlight(cp.isStart && cp.raceInProgress);
- else if (cp.mStart != null && !cp.mStart.raceInProgress) cp.SetHighlight(false);
- }
- }
- else if (ent.isFinished || !raceInProgress)
- {
- foreach (var cp in mList)
- {
- if (cp.track == track) cp.SetHighlight(false);
- else if (cp.mStart != null && !cp.mStart.raceInProgress) cp.SetHighlight(false);
- }
- }
- else
- {
- if (ent.lastCP == mEnd.index)
- {
- foreach (var cp in mList)
- {
- if (cp.track == track) cp.SetHighlight(cp.isStart && cp.raceInProgress);
- else if (cp.mStart != null && !cp.mStart.raceInProgress) cp.SetHighlight(false);
- }
- }
- else
- {
- foreach (var cp in mList)
- {
- if (cp.track == track) cp.SetHighlight(cp.mPrev != null && cp.mPrev.index == ent.lastCP && cp.raceInProgress);
- else if (cp.mStart != null && !cp.mStart.raceInProgress) cp.SetHighlight(false);
- }
- }
- }
- }
- void SetHighlight (bool highlight)
- {
- mHighlighted = highlight;
- if (mLight != null) mLight.enabled = highlight;
- }
- /// <summary>
- /// If all players have reached the end, the race should end.
- /// </summary>
- void CheckIfOver ()
- {
- // The tno.isMine check ensures that this code is executed only on one player: the player that controls the checkpoint object in this case.
- // Without this check all players would be performing the checks below, and multiple end race notificatins would be sent out.
- if (tno.isMine)
- {
- foreach (var ent in mRacers) if (!ent.isFinished) return;
- EndRace();
- }
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment