- Home /
Programming Accurate Clock Hands
So, I recently followed a tutorial on scripting the simple hands of a clock. However, I want to build further on the program by making the hands point accurately relative to the other hands, just like a real-life analog clock.
Here's the current code I have for the Clock Animator:
public class ClockAnimator : MonoBehaviour {
private const float
hoursToDegrees = 360f / 12f,
minutesToDegrees = 360f / 60f,
secondsToDegrees = 360f / 60f;
public Transform hours, minutes, seconds;
// Update is called once per frame
void Update () {
DateTime time = DateTime.Now;
hours.localRotation = Quaternion.Euler(0f, 0f, time.Hour * -hoursToDegrees);
minutes.localRotation = Quaternion.Euler(0f, 0f, time.Minute * -minutesToDegrees);
seconds.localRotation = Quaternion.Euler(0f, 0f, time.Second * -secondsToDegrees);
}
What should I do to start on this?
Thanks!
EDIT: Thanks for everyone who helped with the solution!
Here's what I got:
public class ClockAnimator : MonoBehaviour
{
private const float
hoursToDegrees = 360f / 12f,
minutesToDegrees = 360f / 60f,
secondsToDegrees = 360f / 60f;
public float rHours, rMinutes, rSeconds = 0;
int iHours, iMinutes, iSeconds;
public Transform hours, minutes, seconds;
public bool analog = true; // Smooth == true, Rigid TickTock == false.
[Header("Custom Time")]
public bool useCustomTime = true;
public bool activeTime = true;
public float m_timeAmplifier = 20f;
public DayCycle m_customTime = new DayCycle();
[Serializable]
public class DayCycle
{
[SerializeField]
public float m_time;
private float m_hours;
private float m_minutes;
private float m_seconds;
public float Hours { get { return Mathf.Repeat(m_time / 3600f, 24f); } }
public float Minutes { get { return Mathf.Repeat(m_time / 60f, 60f); } }
public float Seconds { get { return Mathf.Repeat(m_time, 60f); } }
public bool militaryTime = false;
public void AddSeconds(float durationInSeconds)
{
m_time += durationInSeconds;
if (m_time > 86400) // == 60*60*24 wrap around after a day.
m_time -= 86400;
//if (m_time > 43200) //
// m_time -= 43200;
}
public void AddMinutes(float durationInMinutes) { AddSeconds(durationInMinutes * 60f); }
public void AddHours(float durationInHours) { AddSeconds(durationInHours * 3600f); }
public void SetTime(int aHour, int aMinute, int aSecond)
{
//if (!militaryTime)
//{
// if (aHour > 12)
// {
// aHour -= 12;
// }
// if (aHour < 1)
// {
// aHour = 12;
// }
//}
m_time = (aHour * 60f + aMinute) * 60f + aSecond;
Debug.Log("Set Time = (" + aHour + "* 60 " + "+ " + aMinute + ")" + " * 60 + " + aSecond + " = " + m_time);
}
public void SetTime(float hours, float minutes, float seconds)
{
m_hours = 0.0f;
m_minutes = 0.0f;
m_seconds = 0.0f;
AddHours(hours);
AddMinutes(minutes);
AddSeconds(seconds);
}
/*[SerializeField]
private float m_hours = 7, m_minutes = 5, m_seconds = 23;
public float Hours { get { return m_hours; } set { m_hours = Mathf.Repeat( value, 24.0f ); } }
public float Minutes { get { return m_minutes; } set { m_minutes = Mathf.Repeat( value, 60.0f ); } }
public float Seconds { get { return m_seconds; } set { m_seconds = Mathf.Repeat( value, 60.0f ); } }
private const float m_hourInSeconds = 3600.0f,
m_minuteInSeconds = 60.0f;
public void AddMinutes(float durationInMinutes) { AddSeconds( durationInMinutes * m_minuteInSeconds ); }
public void AddHours( float durationInHours ) { AddSeconds( durationInHours * m_hourInSeconds ); }
public void RandomizeTime()
{
AddSeconds(UnityEngine.Random.Range(0f, 60f));
AddMinutes(UnityEngine.Random.Range(0f, 60f));
AddHours(UnityEngine.Random.Range(0f, 12f));
}*/
}
void Start()
{
if(!activeTime)
{
//m_customTime.RandomizeTime();
rSeconds = UnityEngine.Random.Range(0, 60);
rMinutes = UnityEngine.Random.Range(0, 60);
rHours = UnityEngine.Random.Range(0, 12);
//m_customTime.SetTime(UnityEngine.Random.Range(0, 60), UnityEngine.Random.Range(0, 60), UnityEngine.Random.Range(0, 12));
m_customTime.SetTime(rSeconds, rMinutes, rHours);
}
}
void RandomizeTime()
{
rSeconds = UnityEngine.Random.Range(0, 60);
rMinutes = UnityEngine.Random.Range(0, 60);
rHours = UnityEngine.Random.Range(0, 12);
//m_customTime.SetTime(UnityEngine.Random.Range(0, 60), UnityEngine.Random.Range(0, 60), UnityEngine.Random.Range(0, 12));
m_customTime.SetTime(rSeconds, rMinutes, rHours);
}
void DisplayTime(float _seconds, float _minutes, float _hours)
{
if (m_customTime.militaryTime) { Debug.Log(ToMilitaryString(_seconds, _minutes, _hours)); }
else { Debug.Log("Display Time: " + ToStandardTime(_seconds, _minutes, _hours)); }
}
void DisplayTime()
{
if (m_customTime.militaryTime) { Debug.Log(ToMilitaryString(rSeconds, rMinutes, rHours)); }
else { Debug.Log("Display Time: " + ToStandardTime(rSeconds, rMinutes, rHours)); }
}
void Update()
{
if (Input.GetKey(KeyCode.R))
{
//m_customTime.SetTime(UnityEngine.Random.Range(0, 60), UnityEngine.Random.Range(0, 60), UnityEngine.Random.Range(0, 24));
rSeconds = UnityEngine.Random.Range(0, 60);
rMinutes = UnityEngine.Random.Range(0, 60);
rHours = UnityEngine.Random.Range(0, 12);
//m_customTime.SetTime(UnityEngine.Random.Range(0, 60), UnityEngine.Random.Range(0, 60), UnityEngine.Random.Range(0, 12));
m_customTime.SetTime(rSeconds, rMinutes, rHours);
}
if (useCustomTime)
{
if (activeTime)
m_customTime.AddSeconds(Time.deltaTime * m_timeAmplifier);
int h = Mathf.FloorToInt(m_customTime.Hours);
int m = Mathf.FloorToInt(m_customTime.Minutes);
int s = Mathf.FloorToInt(m_customTime.Seconds);
Debug.Log("Line163");
Debug.Log("Time is " + h.ToString("00") + ":" + m.ToString("00") + ":" + s.ToString("00"));
//Debug.LogFormat("{0};{1};{2}", m_customTime.Hours.ToString("00"), m_customTime.Minutes.ToString("00"), m_customTime.Seconds.ToString("00"));
//Debug.Log("Line 165: Time = (" + m_customTime.Hours + "h * 60 " + "+ " + m_customTime.Minutes + "m)" + " * 60 + " + m_customTime.Seconds + "s = " + m_customTime.m_time);
if (analog)
{
hours.localRotation =
Quaternion.Euler(0f, 0f, m_customTime.Hours * -hoursToDegrees);
minutes.localRotation =
Quaternion.Euler(0f, 0f, m_customTime.Minutes * -minutesToDegrees);
seconds.localRotation =
Quaternion.Euler(0f, 0f, m_customTime.Seconds * -secondsToDegrees);
//hours.localRotation =
// Quaternion.Euler(0f, 0f, m_customTime.Hours * -hoursToDegrees);
//minutes.localRotation =
// Quaternion.Euler(0f, 0f, m_customTime.Minutes * -minutesToDegrees);
//seconds.localRotation =
// Quaternion.Euler(0f, 0f, m_customTime.Seconds * -secondsToDegrees);
}
else
{
hours.localRotation =
Quaternion.Euler(0f, 0f, Mathf.RoundToInt(m_customTime.Hours) * -hoursToDegrees);
minutes.localRotation =
Quaternion.Euler(0f, 0f, Mathf.RoundToInt(m_customTime.Minutes) * -minutesToDegrees);
seconds.localRotation =
Quaternion.Euler(0f, 0f, Mathf.RoundToInt(m_customTime.Seconds) * -secondsToDegrees);
}
if (m_customTime.militaryTime) { Debug.Log(ToMilitaryString()); }
else { Debug.Log(ToStandardTime()); }
}
else
{
if (analog)
{
TimeSpan timespan = DateTime.Now.TimeOfDay;
hours.localRotation =
Quaternion.Euler(0f, 0f, (float)timespan.TotalHours * -hoursToDegrees);
minutes.localRotation =
Quaternion.Euler(0f, 0f, (float)timespan.TotalMinutes * -minutesToDegrees);
seconds.localRotation =
Quaternion.Euler(0f, 0f, (float)timespan.TotalSeconds * -secondsToDegrees);
}
else
{
DateTime time = DateTime.Now;
hours.localRotation = Quaternion.Euler(0f, 0f, time.Hour * -hoursToDegrees);
minutes.localRotation = Quaternion.Euler(0f, 0f, time.Minute * -minutesToDegrees);
seconds.localRotation = Quaternion.Euler(0f, 0f, time.Second * -secondsToDegrees);
}
DateTime currentTime = DateTime.Now;
DisplayTime();
//Debug.LogFormat("{0};{1};{2}", currentTime.Hour.ToString("00"), currentTime.Minute.ToString("00"), currentTime.Second.ToString("00"));
}
}
public string ToMilitaryString() // Otherwise known as digital representation I suppose.
{
//return string.Format("{0};{1};{2}", Hours.ToString("00"), Minutes.ToString("00"), Seconds.ToString("00"));
return string.Format("{0};{1};{2}", m_customTime.Hours.ToString("00"), m_customTime.Minutes.ToString("00"), m_customTime.Seconds.ToString("00"));
}
public string ToMilitaryString(float _seconds, float _minutes, float _hours) // Otherwise known as digital representation I suppose.
{
//return string.Format("{0};{1};{2}", Hours.ToString("00"), Minutes.ToString("00"), Seconds.ToString("00"));
return string.Format("{0};{1};{2}", m_customTime.Hours.ToString("00"), m_customTime.Minutes.ToString("00"), m_customTime.Seconds.ToString("00"));
}
public string ToStandardTime()
{
float clampedHours = m_customTime.Hours % 12; // Get rest time of 0 to 12, AM or PM.
string partOfDay = m_customTime.Hours > 12 ? "PM" : "AM";
// Should result in "10:34 AM" or "07:48 PM".
// Optionally you can get rid of using clamped hours if you just want to have the AM / PM postfix.
return string.Format("{0};{1};{2}", clampedHours.ToString("00"), m_customTime.Minutes.ToString("00"), m_customTime.Seconds.ToString("00")) + " " + partOfDay;
}
public string ToStandardTime(float _seconds, float _minutes, float _hours)
{
float clampedHours = m_customTime.Hours % 12; // Get rest time of 0 to 12, AM or PM.
string partOfDay = m_customTime.Hours > 12 ? "PM" : "AM";
// Should result in "10:34 AM" or "07:48 PM".
// Optionally you can get rid of using clamped hours if you just want to have the AM / PM postfix.
return string.Format("{0};{1};{2}", clampedHours.ToString("00"), m_customTime.Minutes.ToString("00"), m_customTime.Seconds.ToString("00")) + " " + partOfDay;
}
}
The code you have there as well as the final code of the tutorial in Analog mode seem to be very accurate as far as the hands go. I'm not sure what you mean with
"making the hands point accurately relative to the other hands"
Because it seems like that's already the case. could you elaborate? .
When the seconds hand is moving along a real clock, the $$anonymous$$utes hand realistically doesn't stay in place like in the Unity scene. It moves very subtly to the next $$anonymous$$ute. You will often see the $$anonymous$$ute hand between $$anonymous$$utes as seconds go by. When the seconds hand finally reaches 60, THEN the $$anonymous$$ute hand is on the next $$anonymous$$ute.
This also applies for hours in relation to $$anonymous$$utes, days in relation to hours, etc.
Are Time.hour, Time.$$anonymous$$ute or Time.second giving you only whole numbers or numbers with values after the decimal? If the latter then use $$anonymous$$athf.Floor(Time.second) etc.
Answer by ThePersister · Nov 15, 2016 at 09:49 AM
Hey @Czar-Man, I upgraded the package for you! This was actually quite a bit tougher than I thought!
Preview: https://gyazo.com/3cbbd5e7615946210b4a4e37fc31ade2
Updated Package: https://www.dropbox.com/s/q0rrntk361wreot/ClockExample_V2.unitypackage?dl=0
Changelist:
Now able to set custom time.
Now able to set seconds, minutes and hours individually.
Now able to increment custom time with X seconds, minutes and hours.
Now able to set time amplifier that speeds up custom time! (also works with negative values)
Now debugs current time, remove it if you wish.
Now takes looping into consideration when incrementing / setting time.
Original tutorial as from OP. (for other users)
https://unity3d.com/learn/tutorials/topics/scripting/simple-clock
using UnityEngine;
using System;
public class ClockAnimator : MonoBehaviour
{
private const float
hoursToDegrees = 360f / 12f,
minutesToDegrees = 360f / 60f,
secondsToDegrees = 360f / 60f;
public Transform hours, minutes, seconds;
public bool analog = true; // Smooth == true, Rigid TickTock == false.
[Header("Custom Time")]
public bool useCustomTime = true;
public float m_timeAmplifier = 20f;
public DayCycle m_customTime = new DayCycle();
[Serializable]
public class DayCycle
{
[SerializeField]
private float m_hours = 7, m_minutes = 5, m_seconds = 23;
public float Hours { get { return m_hours; } set { m_hours = Mathf.Repeat( value, 24.0f ); } }
public float Minutes { get { return m_minutes; } set { m_minutes = Mathf.Repeat( value, 60.0f ); } }
public float Seconds { get { return m_seconds; } set { m_seconds = Mathf.Repeat( value, 60.0f ); } }
private const float m_hourInSeconds = 3600.0f,
m_minuteInSeconds = 60.0f;
public void AddSeconds(float durationInSeconds)
{
Hours += durationInSeconds / m_hourInSeconds;
Minutes += durationInSeconds / m_minuteInSeconds;
Seconds += durationInSeconds;
}
public void AddMinutes(float durationInMinutes) { AddSeconds( durationInMinutes * m_minuteInSeconds ); }
public void AddHours( float durationInHours ) { AddSeconds( durationInHours * m_hourInSeconds ); }
}
void Update()
{
if( useCustomTime )
{
m_customTime.AddSeconds( Time.deltaTime * m_timeAmplifier );
Debug.LogFormat( "{0};{1};{2}", m_customTime.Hours.ToString( "00" ), m_customTime.Minutes.ToString( "00" ), m_customTime.Seconds.ToString( "00" ) );
if( analog )
{
hours.localRotation =
Quaternion.Euler( 0f, 0f, m_customTime.Hours * -hoursToDegrees );
minutes.localRotation =
Quaternion.Euler( 0f, 0f, m_customTime.Minutes * -minutesToDegrees );
seconds.localRotation =
Quaternion.Euler( 0f, 0f, m_customTime.Seconds * -secondsToDegrees );
}
else
{
hours.localRotation =
Quaternion.Euler( 0f, 0f, Mathf.RoundToInt( m_customTime.Hours ) * -hoursToDegrees );
minutes.localRotation =
Quaternion.Euler( 0f, 0f, Mathf.RoundToInt( m_customTime.Minutes ) * -minutesToDegrees );
seconds.localRotation =
Quaternion.Euler( 0f, 0f, Mathf.RoundToInt( m_customTime.Seconds ) * -secondsToDegrees );
}
}
else
{
if( analog )
{
TimeSpan timespan = DateTime.Now.TimeOfDay;
hours.localRotation =
Quaternion.Euler( 0f, 0f, ( float )timespan.TotalHours * -hoursToDegrees );
minutes.localRotation =
Quaternion.Euler( 0f, 0f, ( float )timespan.TotalMinutes * -minutesToDegrees );
seconds.localRotation =
Quaternion.Euler( 0f, 0f, ( float )timespan.TotalSeconds * -secondsToDegrees );
}
else
{
DateTime time = DateTime.Now;
hours.localRotation = Quaternion.Euler( 0f, 0f, time.Hour * -hoursToDegrees );
minutes.localRotation = Quaternion.Euler( 0f, 0f, time.Minute * -minutesToDegrees );
seconds.localRotation = Quaternion.Euler( 0f, 0f, time.Second * -secondsToDegrees );
}
DateTime currentTime = DateTime.Now;
Debug.LogFormat( "{0};{1};{2}", currentTime.Hour.ToString( "00" ), currentTime.Minute.ToString( "00" ), currentTime.Second.ToString( "00" ) );
}
}
}
To set the time, just use:
// For Time Setting.
m_customTime.Minutes = 30f;
// For Time Adding.
m_customTime.AddSeconds( 40f );
I hope that's what you're looking for. If it is, please accept this answer, this time forever! And have a nice day! :)
This works really well.
What can I do to manually or randomly adjust each hand to a different time so that the hands adjust accurately relative to each other?
I recently added a function that randomizes each clock hand.
public void RandomizeTime()
{
AddSeconds(UnityEngine.Random.Range(0f, 60f));
Add$$anonymous$$inutes(UnityEngine.Random.Range(0f, 60f));
AddHours(UnityEngine.Random.Range(0f, 12f));
}
However, after each of the hands are randomized, they are not automatically adjusted relative to the lower hands' location.
Example of ideal scenario: If the time is 2:30, then the second hand should be pointing up at 12, the $$anonymous$$ute hand at 30, and the hour hand should be pointing straight in the middle of 2 and 3.
If it's too late to get this answered here, should I make another question?
I don't quite get what you're asking. When you set the time to 2:30:00 your 3 hands will reflect that just fine if you set the "analog" boolean to "true".
edit
Ouh, i see now what you mean. I first thought he uses the DateTime class from the system namespace but he uses his own class. The way the "DayCycle" class is implemented is quite bad as you can set a time out of sync, which makes not much sense ^^. It's much more common to store the time as an absolute seconds value starting at 00:00:00. That way you can simply divide by 60 followed by modulo 60 to get $$anonymous$$utes and divide by 3600 to get hours.
Note: The "non-analog" mode in this answer gives you the wrong result. He should have used FloorToInt
ins$$anonymous$$d of RoundToInt
in line 62, 64 and 66. Rounding will make a hand advance to the next $$anonymous$$ute / hour too early. An "hour value" of "2.5" (== 02:30) would be rounded up to "3" which is nonsense. To get the correct "jumping" behaviour the value always need to be rounded down (hence the FloorToInt).
edit 2
The TimeSpan class can already return fractional $$anonymous$$utes and hours by using Total$$anonymous$$inutes and TotalHours ins$$anonymous$$d. Total$$anonymous$$inutes might not be in the range 0-60 since it represents the total amount of $$anonymous$$utes, but that shouldn't hurt as every additional hour just adds 60 $$anonymous$$utes so just another full rotation. So with the time "02:30:15" Total$$anonymous$$inutes would return "150.25" --> 02 * 60 = 120 + 30 --> 150. And 15 seconds are 0.25 $$anonymous$$utes (15/60). I think he made his answer more complicated than it needs to be ^^.
The easiest fix in this answer would be rewriting the DayCycle class like this
public class DayCycle
{
[SerializeField]
private float m_time;
public float Hours { get { return $$anonymous$$athf.Repeat(m_time/3600f, 24f); } }
public float $$anonymous$$inutes { get { return $$anonymous$$athf.Repeat(m_time/60f, 60f); } }
public float Seconds { get { return $$anonymous$$athf.Repeat(m_time, 60f); } }
public void AddSeconds(float durationInSeconds)
{
m_time += durationInSeconds;
if (m_time > 86400) // == 60*60*24 wrap around after a day.
m_time -= 86400;
}
public void Add$$anonymous$$inutes(float durationIn$$anonymous$$inutes)
{
AddSeconds( durationIn$$anonymous$$inutes * 60f );
}
public void AddHours( float durationInHours )
{
AddSeconds( durationInHours * 3600f );
}
public void SetTime(int aHour, int a$$anonymous$$inute, int aSecond)
{
m_time = (aHour * 60f + a$$anonymous$$inute) * 60f + aSecond;
}
}
I removed the setter of the Hours, $$anonymous$$inutes and Seconds properties since it makes not much sense to allow setting a fractional hour. I've added a seperate SetTime method which allows you to set an absolute time.
Nice adjustments @Bunny83 ! The answer is complicated indeed. It came down to @Czar-$$anonymous$$an wanting to set a custom time manually. Hence the custom class and the allowance of setting Hours, $$anonymous$$inutes and Seconds. It made sense to me :)
$$anonymous$$y bad about rounding the int ins$$anonymous$$d of flooring it, you're absolutely right!
Good job on helping Czar-$$anonymous$$an out. I hope that's all he needs. Looks like Bunny has got you from here, best of luck!
Got it working well.
Now I have a few more concerns:
I find that sometimes the hands, the hour hands especially, don't match the correct numbers occasionally when the time is set randomly. [Here's the screenshot]http://answers.unity3d.com/storage/temp/82690-screenshot-2016-11-22-181124.png[1] The Hour hand should be above 9, but is slightly below it.
Edit 11/26/16: From looking at the debug, I think somehow the numbers are being rounded upwards.
Line 165: Time = (7.543056h * 60 + 32.58334m) * 60 + 35s = 27155
What I see put on display as the time is 8:33:35
Any ideas on how to stop the code from rounding itself?
Edit 2 11/26/16: I suspect that the code Hours.ToString("00") is the main culprit behind rounding it up. How does it work and is there any alternative code that merely hides the decimals ins$$anonymous$$d of affecting them?
What can I do to change the time from $$anonymous$$ilitary(00:00 to 23:59) to Standard Time(12:00 to 12:00)? I know that you should subtract 12 from the hour when it's greater than 12, but I don't know where exactly to place it.
What is the formula that can be used to get the time?
I'll ask more questions when they come to me. [1]: http://answers.unity3d.com/storage/temp/82690-screenshot-2016-11-22-181124.png
Hi @Czar-$$anonymous$$an ,
(1) In the screenshot that you're showing us, it's 08:54, as in, not yet 9 o'clock, so it makes sense that the shorter hand is not yet beyond 9. You should note that the red hand is the one indicating seconds, if that's how you set it up at least. In that case, all is well and you might've just mixed up the seconds and $$anonymous$$utes hands in your head. :)
(2 & 3) Using bunny's code, you could add the following get time methods:
public string To$$anonymous$$ilitaryString() // Otherwise known as digital representation I suppose.
{
return string.Format( "{0};{1};{2}", Hours.ToString( "00" ), $$anonymous$$inutes.ToString( "00" ), Seconds.ToString( "00" ) );
}
public string ToStandardTime()
{
float clampedHours = Hours % 12; // Get rest time of 0 to 12, A$$anonymous$$ or P$$anonymous$$.
string partOfDay = Hours > 12 ? "P$$anonymous$$" : "A$$anonymous$$";
// Should result in "10:34 A$$anonymous$$" or "07:48 P$$anonymous$$".
// Optionally you can get rid of using clamped hours if you just want to have the A$$anonymous$$ / P$$anonymous$$ postfix.
return string.Format( "{0};{1};{2}", clampedHours.ToString( "00" ), $$anonymous$$inutes.ToString( "00" ), Seconds.ToString( "00" ) ) + " " + partOfDay;
}
I hope that helps! :)
Uhm "21:54" is "9:54 pm" ^^ so almost "10" (or 22)
"8:54 pm" would be "20:54"