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!
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); } } }
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); }
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
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.
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
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
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
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
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?
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.
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.
@@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 &&
@@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
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?
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
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
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?
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
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!
Thank you for sharing this info and leaving the kind comment.
Your comment was as helpful as the video!
please can you pass the lines you need to modificated?
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);
}
}
}
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.
One of the best unity tutorials I have come across so far. Everything is to the point and clear. Thanks!
Much appreciated for the kind words
You blessed Ludwig for all of us
I was looking for this exactly! Amazing video
Happy I was able to help
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);
}
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
I tried your modification and I got some glitches. So as others commented I went for the Quaternion and it workd!
Amazing! You could make all sorts of cool stuff with this simple system
wow dude this is solid. always wondered how ghosts work, can't wait to try it
Thank you for the comment
Hope it works for your project
Incredible. Super clean and easy to understand! Thanks a lot for the help!
Much appreciated for the kind comment
Underrated
Thanks
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.
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
Is this how to become an oiler
Thank you so much :)
you are welcome
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
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
@@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!
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
cr1tikal
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?
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.
@@fumetsuhito5561 hey thanks for the reference! i was having some issues but I fixed them all :)
thanks again for the video
Is there a particular reason for the bitwise AND on line 37 of the GhostPlayer?
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.
@@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 &&
@@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
Great video, but Whenever I load my scene using the scene manager the ghost isn't there. Do you know how to fix this?
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?
Is there any way to Start the Ghost Replay on Button Press?
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
thank you so much
You are welcome
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
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?
dont know why but the ghost only appear in the last position recorded... not going throgh all positions
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
5:26
line 54 : transform.eulerangles = ghost. Position[index] ?
why not ghost.rotation
i changed ghost.position to rotation and it work perfect
You correct it should it is better to be ghost.rotation
Bigger fonts plz... this is hard to follow along on mobile..
Do you mean the code itself is small or the notes that appear?
@@fumetsuhito5561 Everything. Try watching this on a phone and read stuff. I would increase font size of visual studio and unity or windows
@@DerClaudius I see. I keep that in my mind for the next video. Appreciate the feedback
How the fuck do you have so much money