ArenMook

Race Track mod script for Sightseer

Jan 16th, 2018
287
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 16.67 KB | None | 0 0
  1. using UnityEngine;
  2. using TNet;
  3.  
  4. /// <summary>
  5. /// Race track checkpoint. Has all the functionality needed to have a functional race.
  6. /// </summary>
  7.  
  8. [RequireComponent(typeof(OutpostObject))]
  9. public class Checkpoint : TNBehaviour
  10. {
  11.     // Using 'Tooltip' attribute adds hover-over tooltips in Unity
  12.     [Tooltip("Renderer with the main texture that will be getting replaced")]
  13.     public Renderer flagRenderer;
  14.  
  15.     [Tooltip("Used when this is not the next checkpoint")]
  16.     public Texture2D plainTexture;
  17.  
  18.     [Tooltip("Used to mark the next checkpoint that the player must reach")]
  19.     public Texture2D checkpointTexture;
  20.  
  21.     static List<Checkpoint> mList = new List<Checkpoint>();
  22.     static List<Checkpoint> mRaces = new List<Checkpoint>();
  23.  
  24.     /// <summary>
  25.     /// Any class with non-reference members can be sent over the network. The 'vehicle' field is a reference, so it can't be sent.
  26.     /// Instead, a 'vehicleID' identifier can be sent, allowing us to recover the 'vehicle' reference as needed.
  27.     /// </summary>
  28.  
  29.     [System.Serializable]
  30.     class Entry
  31.     {
  32.         [System.NonSerialized] public Vehicle vehicle;
  33.         public ulong vehicleID;
  34.         public int lastCP = 0;
  35.         public int laps = 0;
  36.         public long start = 0;
  37.         public long lapStart = 0;
  38.         public float bestLap = float.MaxValue;
  39.         public float totalTime = 0f;
  40.         public bool isFinished = false;
  41.     }
  42.  
  43.     List<Entry> mRacers = new List<Entry>();
  44.     OutpostObject mObj;
  45.     Checkpoint mStart;
  46.     Checkpoint mEnd;
  47.     Checkpoint mPrev;
  48.     Checkpoint mNext;
  49.     Light mLight;
  50.     bool mHighlighted = false;
  51.  
  52.     /// <summary>
  53.     /// [GameOption] attribute makes fields show up when the object is right-clicked in the game.
  54.     /// [Validator] attribute specifies a boolean-returning property that will be checked that determines whether the field will be shown.
  55.     /// [GameTooltip] attribute adds a localization string keyword that will be shown when hovering over the field in the game.
  56.     /// Note that the property name (in this case 'track') must also be a localized string in the Localization.csv file.
  57.     /// </summary>
  58.    
  59.     [SkipRename, GameOption, Validator("canEdit"), GameTooltip("raceTrackInfo")]
  60.     public int track { get { return Get<int>("track"); } set { if (value != track && value > -1) Set("track", value); } }
  61.  
  62.     [SkipRename, GameOption, Validator("canEdit"), GameTooltip("raceIndexInfo")]
  63.     public int index { get { return Get<int>("index"); } set { if (value != index && value > -1) Set("index", value); } }
  64.  
  65.     [SkipRename, GameOption, Validator("canStart"), GameTooltip("raceLapsInfo")]
  66.     public int laps { get { return (mStart != null && mStart != this) ? mStart.laps : Get("laps", 1); } set { if (value != index && value > 0) Set("laps", value); } }
  67.  
  68.     [SkipRename] bool isInteractable { get { return mObj != null && mObj.isInteractable; } }
  69.     [SkipRename] bool canEdit { get { return isInteractable && !raceInProgress; } }
  70.     [SkipRename] bool isStart { get { return index == 0 && Find(track, index) == this; } }
  71.     [SkipRename] bool canStart { get { return isInteractable && isStart && !raceInProgress; } }
  72.     [SkipRename] bool canEnd { get { return isInteractable && raceInProgress; } }
  73.     [SkipRename] bool raceInProgress { get { return (mStart != null && mStart != this) ? mStart.raceInProgress : Get<bool>("active"); } }
  74.  
  75.     /// <summary>
  76.     /// OnStart function is the best place to cache references to required components and subscribe to events.
  77.     /// </summary>
  78.  
  79.     public override void OnStart ()
  80.     {
  81.         base.OnStart();
  82.         mObj = GetComponent<OutpostObject>();
  83.         mLight = GetComponentInChildren<Light>();
  84.         if (mLight != null) mLight.enabled = false;
  85.         flagRenderer.AddOnRender(OnRender);
  86.         tno.onDataChanged += OnSetData;
  87.     }
  88.  
  89.     /// <summary>
  90.     /// When the object's data changes, relink and update the highlight state of all the checkpoints.
  91.     /// </summary>
  92.  
  93.     void OnSetData (DataNode data)
  94.     {
  95.         foreach (var cp in mList) if (cp.isStart) cp.Link();
  96.         if (mStart != null) mStart.UpdateHighlight();
  97.     }
  98.  
  99.     /// <summary>
  100.     /// 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).
  101.     /// 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.
  102.     /// </summary>
  103.  
  104.     void OnRender (MaterialPropertyBlock block)
  105.     {
  106.         if (mHighlighted) block.SetTexture("_MainTex", checkpointTexture);
  107.         else block.SetTexture("_MainTex", plainTexture);
  108.     }
  109.  
  110.     // 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.
  111.     void OnEnable () { mList.Add(this); }
  112.     void OnDisable () { mList.Remove(this); }
  113.  
  114.     /// <summary>
  115.     /// 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'.
  116.     /// </summary>
  117.  
  118.     bool Link (Checkpoint start = null)
  119.     {
  120.         if (start == null) start = this;
  121.  
  122.         var end = start;
  123.  
  124.         for (int i = 0; i < mList.size; ++i)
  125.         {
  126.             var cp = mList.buffer[i];
  127.             if (cp.track != track) continue;
  128.             cp.mStart = start;
  129.             cp.mPrev = cp.isStart ? null : cp.FindPrevious();
  130.             cp.mNext = cp.FindNext();
  131.             if (cp.mNext != null && end.index < cp.mNext.index) end = cp.mNext;
  132.         }
  133.  
  134.         for (int i = 0; i < mList.size; ++i)
  135.         {
  136.             var cp = mList.buffer[i];
  137.             if (cp.track != track) continue;
  138.             cp.mEnd = end;
  139.         }
  140.         return end != start;
  141.     }
  142.  
  143.     Checkpoint FindNext ()
  144.     {
  145.         Checkpoint best = null;
  146.  
  147.         foreach (var c in mList)
  148.         {
  149.             if (c.track != track) continue;
  150.             if (c == this) continue;
  151.             if (c.index <= index) continue;
  152.             if (best == null || best.index > c.index) best = c;
  153.         }
  154.         return best;
  155.     }
  156.  
  157.     Checkpoint FindPrevious ()
  158.     {
  159.         Checkpoint best = null;
  160.  
  161.         foreach (var c in mList)
  162.         {
  163.             if (c.track != track) continue;
  164.             if (c == this) continue;
  165.             if (c.index >= index) continue;
  166.             if (best == null || best.index < c.index) best = c;
  167.         }
  168.         return best;
  169.     }
  170.  
  171.     /// <summary>
  172.     /// [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.
  173.     /// </summary>
  174.  
  175.     [SkipRename, GameOption, Validator("canStart")]
  176.     void StartRace ()
  177.     {
  178.         if (Link())
  179.         {
  180.             // 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).
  181.             Set("active", true);
  182.  
  183.             // 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).
  184.             // 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
  185.             // one-time notification for all players.
  186.             tno.Send("OnStartRace", Target.All);
  187.         }
  188.         else
  189.         {
  190.             // UIStatusBar lets us show an alert-like notification at the top of the screen. There are also UIStatusBar.Show(), ShowLocalized(), and ShowError().
  191.             UIStatusBar.ShowLocalizedError("No checkpoints", 3f);
  192.         }
  193.     }
  194.  
  195.     [RFC]
  196.     void OnStartRace ()
  197.     {
  198.         if (Link())
  199.         {
  200.             // This is the player's directly controlled vehicle. ControllableEntity.mine would be the vehicle the player is in (but is not necessarily controlling).
  201.             var vehicle = ControllableEntity.controlled;
  202.  
  203.             if (vehicle.GetDistanceTo(transform.position) < 40f)
  204.             {
  205.                 // All FactionObject-derived classes can print text above them via AddHUDText. If you want all players to see this text, use SendHUDText instead.
  206.                 vehicle.AddHUDText(Localization.Get("GO"), Color.green, 4f);
  207.                 GameAudio.Play(GameAudio.instance.boom);
  208.             }
  209.         }
  210.     }
  211.  
  212.     /// <summary>
  213.     /// 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
  214.     /// the starting checkpoint (index of 0). This makes it possible to end the race from any checkpoint.
  215.     /// </summary>
  216.  
  217.     [SkipRename, GameOption, Validator("canEnd")]
  218.     void EndRace ()
  219.     {
  220.         if (mStart != null && mStart != this)
  221.         {
  222.             // We want to only end the race on the same object that can start it: the start checkpoint.
  223.             mStart.EndRace();
  224.         }
  225.         else
  226.         {
  227.             // Notify everyone that the race is ending
  228.             tno.Send("OnEndRace", Target.All);
  229.  
  230.             // This actually ends the race by flipping the 'active' flag
  231.             Set("active", false);
  232.  
  233.             // Clear all references to be on the safe side
  234.             foreach (var cp in mList)
  235.             {
  236.                 if (cp.mStart == this)
  237.                 {
  238.                     cp.mStart = null;
  239.                     cp.mEnd = null;
  240.                     cp.mNext = null;
  241.                     cp.mPrev = null;
  242.                 }
  243.             }
  244.         }
  245.     }
  246.  
  247.     [RFC]
  248.     void OnEndRace ()
  249.     {
  250.         var vehicle = ControllableEntity.controlled as Vehicle;
  251.  
  252.         if (vehicle != null && (FindRacer(vehicle) != null || vehicle.GetDistanceTo(transform.position) < 40f ||
  253.             (mEnd != null && vehicle.GetDistanceTo(mEnd.transform.position) < 40f)))
  254.         {
  255.             // You can print text directly to chat
  256.             UIGameChat.AddCurrent(Localization.Get("endRace"), Color.yellow);
  257.  
  258.             // By default, printing text to chat won't make the window show up if it was minimized. Calling Show() will force it to maximize.
  259.             UIGameChat.Show();
  260.  
  261.             // Sort the list of racers in an ascending order (the sorting function is below)
  262.             mRacers.Sort(SortByTime);
  263.  
  264.             // Print all racers to the chat window as a summary
  265.             for (int i = 0, index = 1; i < mRacers.size; ++i)
  266.             {
  267.                 var racer = mRacers[i];
  268.                 if (racer.totalTime == 0f || racer.vehicle == null) continue;
  269.                 UIGameChat.AddCurrent(Localization.Format("raceSummary", index, racer.vehicle.visibleName,
  270.                     GameTools.HighlightText(GameTools.FormatTimeMS(racer.totalTime)),
  271.                     GameTools.FormatTimeMS(racer.bestLap)), Color.yellow);
  272.                 ++index;
  273.             }
  274.  
  275.             mRaces.Remove(this);
  276.             mRacers.Clear();
  277.         }
  278.     }
  279.  
  280.     static int SortByTime (Entry a, Entry b)
  281.     {
  282.         var fa = (a.totalTime == 0f) ? float.MaxValue : a.totalTime;
  283.         var fb = (b.totalTime == 0f) ? float.MaxValue : b.totalTime;
  284.         return fa.CompareTo(fb);
  285.     }
  286.  
  287.     static Checkpoint Find (int track, int index)
  288.     {
  289.         foreach (var cp in mList) if (cp.track == track && cp.index == index) return cp;
  290.         return null;
  291.     }
  292.  
  293.     Entry FindRacer (Vehicle v)
  294.     {
  295.         if (v == null) return null;
  296.         foreach (var ent in mRacers) if (ent.vehicle == v) return ent;
  297.         return null;
  298.     }
  299.  
  300.     /// <summary>
  301.     /// OnTriggerEnter is called whenever any object enter one of the trigger-marked colliders belonging to this script's rigidbody.
  302.     /// Note that this will only work if this script is attached to the same game object as the rigidbody or the trigger collider itself.
  303.     /// </summary>
  304.  
  305.     void OnTriggerEnter (Collider other)
  306.     {
  307.         // Don't even try to do anything unless a race is actually in progress
  308.         if (!raceInProgress) return;
  309.  
  310.         // Only the controlling player should be performing checks
  311.         var vehicle = other.GetComponentInParent<Vehicle>();
  312.         if (vehicle != null && vehicle.isControlledByMe) Advance(vehicle, this);
  313.     }
  314.  
  315.     /// <summary>
  316.     /// Given the vehicle and the checkpoint it's passing through, progress the race.
  317.     /// </summary>
  318.  
  319.     void Advance (Vehicle vehicle, Checkpoint currentCP)
  320.     {
  321.         if (mStart != null && mStart != this) { mStart.Advance(vehicle, currentCP); return; }
  322.         if (!raceInProgress || vehicle.mainPlayer == null) return;
  323.  
  324.         var ent = FindRacer(vehicle);
  325.  
  326.         // Just in case -- this can trigger if reconnecting while a race is active
  327.         if (mStart == null || mEnd == null) { if (!Link()) return; }
  328.  
  329.         if (ent == null)
  330.         {
  331.             // New racers must begin with the first checkpoint
  332.             if (!currentCP.isStart) return;
  333.  
  334.             ent = new Entry();
  335.             ent.vehicle = vehicle;
  336.             ent.vehicleID = vehicle.uid;
  337.             ent.start = TNManager.serverTime;
  338.             ent.lapStart = ent.start;
  339.             ent.lastCP = 0;
  340.             mRacers.Add(ent);
  341.             OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
  342.             return;
  343.         }
  344.        
  345.         // Wrong checkpoint
  346.         if (ent.isFinished || ent.lastCP == currentCP.index) return;
  347.  
  348.         // Reached the end
  349.         if (currentCP.index == mEnd.index)
  350.         {
  351.             if (laps < 2)
  352.             {
  353.                 // Single lap race -- end point is the end
  354.                 ent.bestLap = (TNManager.serverTime - ent.start) * 0.001f;
  355.                 ent.totalTime = ent.bestLap;
  356.                 ent.isFinished = true;
  357.  
  358.                 var s = Localization.Format("bestTime", GameTools.HighlightText(GameTools.FormatTimeMS(ent.bestLap)));
  359.                 UIGameChat.AddCurrent(s, Color.yellow);
  360.                 UIStatusBar.Show(s, 3f);
  361.                 OnAdvance(vehicle, ent, GameAudio.instance.success);
  362.             }
  363.             else
  364.             {
  365.                 // It's a multi-lap race, the end checkpoint simply wraps around to the beginning
  366.                 ent.lastCP = mEnd.index;
  367.                 OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
  368.             }
  369.         }
  370.         else if (currentCP.index == mStart.index && ent.lastCP == mEnd.index)
  371.         {
  372.             var time = TNManager.serverTime;
  373.             var lapTime = (time - ent.lapStart) * 0.001f;
  374.             ent.totalTime += lapTime;
  375.             if (ent.bestLap > lapTime) ent.bestLap = lapTime;
  376.  
  377.             // Lap complete
  378.             if (++ent.laps < laps)
  379.             {
  380.                 // Next lap
  381.                 ent.lapStart = time;
  382.                 ent.lastCP = mStart.index;
  383.                 UIStatusBar.Show(Localization.Format("lapTime", ent.laps, GameTools.HighlightText(GameTools.FormatTimeMS(lapTime))), 3f);
  384.                 OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
  385.             }
  386.             else
  387.             {
  388.                 // The race is over for this vehicle
  389.                 ent.isFinished = true;
  390.                 UIStatusBar.Show(Localization.Format("bestTime", GameTools.HighlightText(GameTools.FormatTimeMS(ent.bestLap))), 3f);
  391.                 OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
  392.             }
  393.         }
  394.         else if (currentCP.mPrev != null && currentCP.mPrev.index == ent.lastCP)
  395.         {
  396.             ent.lastCP = currentCP.index;
  397.             OnAdvance(vehicle, ent, GameAudio.instance.activate[0]);
  398.         }
  399.     }
  400.  
  401.     void OnAdvance (Vehicle vehicle, Entry ent, AudioClip audio)
  402.     {
  403.         UpdateHighlight(ent);
  404.  
  405.         // You can play sounds in 2D via GameAudio.Play, or in 3D via ControllableEntity.PlaySound.
  406.         vehicle.PlaySound(audio);
  407.  
  408.         // Here we send out a packet to other players, passing the entire Entity class instance. Since this function is only ultimately
  409.         // called by each vehicle's controlling player due to the isControlledByMe check in OnTriggerEnter, this means that each player
  410.         // is responsible for keeing their own record up-to-date and informing everyone else.
  411.         tno.Send("SyncPlayer", Target.Others, ent);
  412.  
  413.         // Check to see if the race should now be over
  414.         CheckIfOver();
  415.     }
  416.  
  417.     [RFC]
  418.     void SyncPlayer (Entry ent)
  419.     {
  420.         // Controllable entities can be vehicles (land / air / sea), and turrets. Since turrets can't move, we only want vehicles here.
  421.         // The soft C# cast (as Something) will simply be 'null' if it can't be converted.
  422.         ent.vehicle = ControllableEntity.Find(ent.vehicleID) as Vehicle;
  423.  
  424.         for (int i = 0; i < mRacers.size; ++i)
  425.         {
  426.             var r = mRacers[i];
  427.  
  428.             if (r.vehicleID == ent.vehicleID)
  429.             {
  430.                 mRacers[i] = ent;
  431.                 CheckIfOver();
  432.                 return;
  433.             }
  434.         }
  435.  
  436.         mRacers.Add(ent);
  437.     }
  438.  
  439.     void UpdateHighlight ()
  440.     {
  441.         UpdateHighlight(raceInProgress ? FindRacer(ControllableEntity.controlled as Vehicle) : null);
  442.     }
  443.  
  444.     /// <summary>
  445.     /// This function determines whether the checkpoint should be highlighted or not. This is what makes the next checkpoint the player must pass through highlighted.
  446.     /// </summary>
  447.  
  448.     void UpdateHighlight (Entry ent)
  449.     {
  450.         if (ent == null)
  451.         {
  452.             foreach (var cp in mList)
  453.             {
  454.                 if (cp.track == track) cp.SetHighlight(cp.isStart && cp.raceInProgress);
  455.                 else if (cp.mStart != null && !cp.mStart.raceInProgress) cp.SetHighlight(false);
  456.             }
  457.         }
  458.         else if (ent.isFinished || !raceInProgress)
  459.         {
  460.             foreach (var cp in mList)
  461.             {
  462.                 if (cp.track == track) cp.SetHighlight(false);
  463.                 else if (cp.mStart != null && !cp.mStart.raceInProgress) cp.SetHighlight(false);
  464.             }
  465.         }
  466.         else
  467.         {
  468.             if (ent.lastCP == mEnd.index)
  469.             {
  470.                 foreach (var cp in mList)
  471.                 {
  472.                     if (cp.track == track) cp.SetHighlight(cp.isStart && cp.raceInProgress);
  473.                     else if (cp.mStart != null && !cp.mStart.raceInProgress) cp.SetHighlight(false);
  474.                 }
  475.             }
  476.             else
  477.             {
  478.                 foreach (var cp in mList)
  479.                 {
  480.                     if (cp.track == track) cp.SetHighlight(cp.mPrev != null && cp.mPrev.index == ent.lastCP && cp.raceInProgress);
  481.                     else if (cp.mStart != null && !cp.mStart.raceInProgress) cp.SetHighlight(false);
  482.                 }
  483.             }
  484.         }
  485.     }
  486.  
  487.     void SetHighlight (bool highlight)
  488.     {
  489.         mHighlighted = highlight;
  490.         if (mLight != null) mLight.enabled = highlight;
  491.     }
  492.  
  493.     /// <summary>
  494.     /// If all players have reached the end, the race should end.
  495.     /// </summary>
  496.  
  497.     void CheckIfOver ()
  498.     {
  499.         // The tno.isMine check ensures that this code is executed only on one player: the player that controls the checkpoint object in this case.
  500.         // Without this check all players would be performing the checks below, and multiple end race notificatins would be sent out.
  501.         if (tno.isMine)
  502.         {
  503.             foreach (var ent in mRacers) if (!ent.isFinished) return;
  504.             EndRace();
  505.         }
  506.     }
  507. }
Advertisement
Add Comment
Please, Sign In to add comment