Unity - A Simple Ghost Replay System

Поділитися
Вставка
  • Опубліковано 21 лис 2024

КОМЕНТАРІ • 59

  • @Asjagadra
    @Asjagadra 3 роки тому +7

    Rotations were not quite right for me, it was a bit glitchy but I ended up fixing it by making a Quaternion list instead of Vector3 and used a Quaternion.Slerp in the GhostPlayer. It works like a charm now!
    Thanks for the amazing tutorial!

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому +1

      Thank you for sharing this info and leaving the kind comment.

    • @erfanroghani
      @erfanroghani 3 роки тому +1

      Your comment was as helpful as the video!

    • @raiden-burst
      @raiden-burst Рік тому

      please can you pass the lines you need to modificated?

  • @erfanroghani
    @erfanroghani 3 роки тому +8

    Thank you so much for keeping it short and to the point. There could sometimes be some glitches that can be avoided by using quaternion as follow;
    using UnityEngine;
    public class GhostRecorder : MonoBehaviour
    {
    public Ghost ghost;
    private float timer;
    private float timeValue;
    private void Awake()
    {
    if (ghost.isRecord)
    {
    ghost.ResetData();
    timeValue = 0;
    timer = 0;
    }
    }
    void Update()
    {
    timer += Time.unscaledDeltaTime;
    timeValue += Time.unscaledDeltaTime;
    if (ghost.isRecord & timer >= 1 / ghost.recordFrequency)
    {
    ghost.timeStamp.Add(timeValue);
    ghost.position.Add(this.transform.position);
    ghost.rotation.Add(this.transform.rotation);
    timer = 0;
    }
    }

    }
    and ...
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    public class GhostPlayer : MonoBehaviour
    {
    public Ghost ghost;
    private float timeValue;
    private int index1;
    private int index2;
    private void Awake()
    {
    timeValue = 0;
    }
    void Update()
    {
    timeValue += Time.unscaledDeltaTime;
    if (ghost.isReplay)
    {
    GetIndex();
    SetTransform();
    }
    }
    private void GetIndex()
    {
    for (int i = 0; i < ghost.timeStamp.Count - 2; i++)
    {
    if (ghost.timeStamp[i] == timeValue)
    {
    index1 = i;
    index2 = i;
    return;
    }
    else if (ghost.timeStamp[i] < timeValue & timeValue < ghost.timeStamp[i + 1])
    {
    index1 = i;
    index2 = i + 1;
    return;
    }
    }
    index1 = ghost.timeStamp.Count - 1;
    index2 = ghost.timeStamp.Count - 1;
    }
    private void SetTransform()
    {
    if (index1 == index2)
    {
    this.transform.position = ghost.position[index1];
    this.transform.rotation = ghost.rotation[index1];
    }
    else
    {
    float interpolationFactor = (timeValue - ghost.timeStamp[index1]) / (ghost.timeStamp[index2] - ghost.timeStamp[index1]);
    this.transform.position = Vector3.Lerp(ghost.position[index1], ghost.position[index2], interpolationFactor);
    this.transform.rotation = Quaternion.Slerp(ghost.rotation[index1], ghost.rotation[index2], interpolationFactor);
    }
    }
    }

  • @megahombre24
    @megahombre24 3 роки тому +5

    This is awesome, great tutorial, straight to the point, easy to follow and to understand, good job! Wish that more tutorials on YT would be like that.

  • @kartikaeyakumar770
    @kartikaeyakumar770 3 роки тому +2

    One of the best unity tutorials I have come across so far. Everything is to the point and clear. Thanks!

  • @beanhub8935
    @beanhub8935 3 роки тому +3

    You blessed Ludwig for all of us

  • @devChrisAP
    @devChrisAP 3 роки тому +3

    I was looking for this exactly! Amazing video

  • @FuraCocoCod
    @FuraCocoCod 3 роки тому +1

    Excellent tutorial, thanks for sharing. I noticed that the ghost gets stuck in the rotation when the Euler exceeds 180 degrees and the object is shaking at times, to fix that I applied the WrapAngle on the euler angle to fix that, it was perfect. Here's the WrapAngle function code for those who want to use it:
    private static float WrapAngle(float angle)
    {
    angle %= 360;
    if (angle > 180)
    return angle - 360;
    return angle;
    }
    to implement in the code looks like this:
    float rotX = WrapAngle(ghost.rotation[index1].x);
    float rotY = WrapAngle(ghost.rotation[index1].y);
    float rotZ = WrapAngle(ghost.rotation[index1].z);
    Vector3 rotFinal = new Vector3(rotX, rotY, rotZ);
    float rotX2 = WrapAngle(ghost.rotation[index2].x);
    float rotY2 = WrapAngle(ghost.rotation[index2].y);
    float rotZ2 = WrapAngle(ghost.rotation[index2].z);
    Vector3 rotFinal2 = new Vector3(rotX2, rotY2, rotZ2);
    if (index1 == index2)
    {
    this.transform.position = ghost.position[index1];
    this.transform.eulerAngles = rotFinal;
    }
    else
    {
    float interpolationFactor = (timeValue - ghost.timeStamp[index1]) / (ghost.timeStamp[index2] - ghost.timeStamp[index1]);
    this.transform.position = Vector3.Lerp(ghost.position[index1], ghost.position[index2], interpolationFactor);
    this.transform.eulerAngles = Vector3.Lerp(rotFinal, rotFinal2, interpolationFactor);
    }

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому +1

      Much appreciated for the comment. Also thank you for sharing your findings, it can definitely help other people. Note there is another users who faces a similar problem and his solution was to use Quaternion list instead of Vector3. the user is
      Asjagadra and his comment is below somewhere

    • @erfanroghani
      @erfanroghani 3 роки тому +1

      I tried your modification and I got some glitches. So as others commented I went for the Quaternion and it workd!

  • @supremespark4755
    @supremespark4755 3 роки тому +2

    Amazing! You could make all sorts of cool stuff with this simple system

  • @loganh6748
    @loganh6748 3 роки тому

    wow dude this is solid. always wondered how ghosts work, can't wait to try it

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому

      Thank you for the comment
      Hope it works for your project

  • @dzmitry5697
    @dzmitry5697 2 роки тому

    Incredible. Super clean and easy to understand! Thanks a lot for the help!

  • @plaidev
    @plaidev 3 роки тому

    Underrated

  • @rodrigolabrada5642
    @rodrigolabrada5642 3 роки тому

    Thank you so much for this information, I would like to ask you a question, in the 1:22 minute, you have 3 scripts and one blue and orange thing on the left called "Ghost", whats that? sorry im a begginer and I think thats the only part that I dont understand from the video.

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому +1

      Thank you for the kind words.
      The orange and blue thing is the scriptable object it self. I suggest you look at the link in the description which will lead you to a talk about scriptable objects and what they are

  • @percentgaming4234
    @percentgaming4234 3 роки тому +7

    Is this how to become an oiler

  • @rajsah4667
    @rajsah4667 2 роки тому

    Thank you so much :)

  • @milanmolnar3820
    @milanmolnar3820 3 роки тому +1

    First of all Thanks for this awesome tutorial.
    I would like to ask a question though. I started to create a racing game, as a hobby project and would like to use this system as a ghost system in the game, but the ghost sometimes starts faster and sometimes starts slower then the player when inputing the same values, can it be that my scene loading sometimes takes longer and sometimes takes less time so the ghost has more or less time to load. How would you tackle this issue. Thanks alot for the efforts its very helpful!
    best regards

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому

      Thank you for the kind comment really appreciate it.
      For your issue it depends on how you set up the logic but I would do/confirm the following.
      1- make it so you load the ghost input in the awake method if you are not already. This is so everything is loaded before the "Play" starts
      2- make sure the timer that the ghost is referencing is similar to the in-game time. I would recommended having the "timer" as a scriptable object and any script that requires the timer value reference this scriptable objects. If you do not know what a scriptable object I recommend watching the video in my description.
      3- make sure the timer is reset to the right value each time you start playing.
      My guess without knowing anything about your code is that the issue is with the timer start value or sync with the actual game. Because you said sometimes it is faster and others slower. So perhaps the timer does not start/reset consistently each time.
      Hope this helps

    • @milanmolnar3820
      @milanmolnar3820 3 роки тому

      @@fumetsuhito5561 I will check out the video in my description about scriptable objects, thanks for the help in such a short notice. Have a nice day!

    • @harrisaamir7555
      @harrisaamir7555 Рік тому

      I was having the same issue as yours and I resolved it by changing unscaled time to only delta time in both ghost player and ghost recorder scripts. That did it for my use case. Hope it helps you too

  • @roughcamel1395
    @roughcamel1395 3 роки тому +1

    cr1tikal

  • @tummytime123
    @tummytime123 3 роки тому

    This is an awesome tutorial, thanks. I would love to learn how to save/load this data from an external file as I'm trying to set up a system where people can watch other people's replays. I tried using the binary formatter but it doesn't play nice with the list variables. Do you have any idea how I could go about a replay save/load system?

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому

      Thanks for the kind words.
      For saving and loading system one recommendation I have is saving in JSON using JSONutility. Here is a link to a good guide which I used myself ua-cam.com/video/uD7y4T4PVk0/v-deo.html
      JSON format is compatible with many interfaces which makes it good. The only set back I see for this method is that the save file is easily modifiable as it is written in human readable format. But at the same time this is powerful as you can easily change setting save data by just changing the text file.

    • @tummytime123
      @tummytime123 3 роки тому +1

      @@fumetsuhito5561 hey thanks for the reference! i was having some issues but I fixed them all :)
      thanks again for the video

  • @milhouse529
    @milhouse529 3 роки тому +1

    Is there a particular reason for the bitwise AND on line 37 of the GhostPlayer?

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому

      Yes, this is so we ensure take the recorded point before and recorded point after the current time value.
      For example, image we have a recorded ghost coordinates at the following time stamps: 0 sec, 1 sec, 2 sec.
      Say we want to interpolate the coordinates at 1.5 sec (timeValue=1.5). The correct way to interpolate is to use the 1 sec value and the 2 sec value. which is index 1 and index 2 in the list.
      if I did not use the AND and only had the condition where "ghost.timeStamp[i] < timeValue" then this condition is true at 0 sec, which will make the code interpolate between 0 sec and 1 sec which is not correct.
      That is why we have the second condition.

    • @milhouse529
      @milhouse529 3 роки тому +1

      @@fumetsuhito5561 I might have written my question improperly. I understood that you are interpolating between two recorded values but was curious why did you use the bitwise AND operator &, not logical operator &&

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому +1

      @@milhouse529 I will be honest I did not know there was a difference between the & and &&. Now I know there is a difference and it is better to use && as you mentioned. Thank you for the tip

  • @mikelanimations138
    @mikelanimations138 2 роки тому

    Great video, but Whenever I load my scene using the scene manager the ghost isn't there. Do you know how to fix this?

    • @fumetsuhito5561
      @fumetsuhito5561  2 роки тому

      Thank you for the comment
      Without seeing your code it might be hard to figure the actual problem. if you want you can share the scripts you have and I can have a quick look
      Also just to understand if enter play mode when the scene is already selected it works, but if you enter play mode then load the scene it does not work?

  • @marvelzs3532
    @marvelzs3532 Рік тому

    Is there any way to Start the Ghost Replay on Button Press?

    • @fumetsuhito5561
      @fumetsuhito5561  Рік тому

      Definitely it is possible, for example you can make it so when the button is pressed the "Is reply" becomes true, then you substract the "timeValue" by the timevalue when the button was pressed

  • @trix7450
    @trix7450 3 роки тому

    thank you so much

  • @manuelpascualgarcia8504
    @manuelpascualgarcia8504 2 роки тому

    i did all what you said and i dont know why the replay object is taking the positions correctly but after the record, in play it only spawns in the last position it was, non going throw all the positions... why?
    also you are creating a dont destroy on load object in play, when did u do that??, no info about the ghost object

    • @fumetsuhito5561
      @fumetsuhito5561  2 роки тому +1

      Greetings,
      For you first question, since you are recording all the positions correctly I would confirm/look at the following points:
      1-Confirm if the time stamps are recorded correctly in the ghost holder
      2-Confrim if the timer ("timeValue" variable in the ghost player script) starts at 0 when you start the replay/scene and that it increments correctly
      3-Confrim that the "GetIndex" method in the ghost player script (video time 5:05) is returning the expected index at each "timeValue" it might be that the "if" condition is slightly different or it is not looping correctly for some reason
      Hope this helps.
      If this does not work, you are welcome to share the script/code with me. Maybe I can help
      For the second question, to be honest I do not remember using the don't destroy on load method on anything. Maybe it was being created by default because of something I was not aware of. And functionally I do not think any part of the replay system needs a do not destroy on load method
      Also "no info about the ghost object", is this a question?

  • @haisse6811
    @haisse6811 2 роки тому

    dont know why but the ghost only appear in the last position recorded... not going throgh all positions

    • @fumetsuhito5561
      @fumetsuhito5561  2 роки тому +1

      Greetings,
      Interesting, below are some points you can confirm
      1-Confirm if the time stamps are recorded correctly in the ghost holder
      2-Confrim if the timer ("timeValue" variable in the ghost player script) starts at 0 when you start the replay/scene and that it increments correctly
      3-Confrim that the "GetIndex" method in the ghost player script (video time 5:05) is returning the expected index at each "timeValue" it might be that the "if" condition is slightly different or it is not looping correctly for some reason
      Hope this helps.
      If this does not work, you are welcome to share the script/code with me. Maybe I can help

  • @홍성찬-j3q
    @홍성찬-j3q Рік тому

    5:26
    line 54 : transform.eulerangles = ghost. Position[index] ?
    why not ghost.rotation

    • @홍성찬-j3q
      @홍성찬-j3q Рік тому

      i changed ghost.position to rotation and it work perfect

    • @fumetsuhito5561
      @fumetsuhito5561  Рік тому

      You correct it should it is better to be ghost.rotation

  • @DerClaudius
    @DerClaudius 3 роки тому

    Bigger fonts plz... this is hard to follow along on mobile..

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому +1

      Do you mean the code itself is small or the notes that appear?

    • @DerClaudius
      @DerClaudius 3 роки тому

      @@fumetsuhito5561 Everything. Try watching this on a phone and read stuff. I would increase font size of visual studio and unity or windows

    • @fumetsuhito5561
      @fumetsuhito5561  3 роки тому +1

      @@DerClaudius I see. I keep that in my mind for the next video. Appreciate the feedback

  • @RektDem
    @RektDem 3 роки тому

    How the fuck do you have so much money