一、准备闹钟模型
(一)下载模型
从Unity资源商店和其他模型网站可以下载到各种各样的闹钟模型。为了帮助大家了解机械钟表的设置原理,建议使用带有时针、分针和秒针的钟表,如下图。

注意:时针、分针和秒针最好是挂在闹钟父物体下的三个独立的子物体。
(二)指针归零
如果模型默认的指针未指向12点的位置,那么我们需要手动将其归零。归零后的钟表指针位置如下:

归零后,要观察并记住Inspector中各指针的本地Transform旋转角度(Rotation),这个角度在接下来的脚本中会用到。比如,我的模型各指针旋转角度如下:



二、令闹钟显示当前时间
在Project窗口中新建一个名为Clock的脚本,挂到闹钟物体上。
首先声明变量,除了时针、分针、秒针三个GameObject变量以外,还要声明一个DateTime变量来获取系统时间。因为后面我们还要声明其他变量,为方便区分,可以在这些变量前面加个Header,如下:
[
Header
(
"Clock Parts"
)
]
public
GameObject
hourHand
;
public
GameObject
minuteHand
;
public
GameObject
secondHand
;
DateTime
currentTime
;
用System.DateTime.Now可以获得当前系统时间。
currentTime.Hour:当前小时数
currentTime.Minute:当前分钟数
currentTime.Second:当前秒数
currentTime
=
System
.
DateTime
.
Now
;
那么,如何将系统时间转换为指针的旋转角度呢?以时针为例,可能有小伙伴认为,直接用系统时间的小时数乘以360度再除以24不就行了吗?
这里有两个问题。首先,虽然1天有24个小时,但机械钟表是12小时制,需要先进行换算。第二,时针旋转的角度不仅受到小时数的影响,也受到分钟数和秒数的影响。比如,14:00:00和14:30:00、14:30:40的时针旋转角度肯定是不同的。
带着这个思路,我们先来写时针旋转角度的脚本。
hourHand
.
transform
.
localRotation
=
UnityEngine
.
Quaternion
.
Euler
(
0.0f
,
0.0f
,
(
90.0f
+
(
(
currentTime
.
Hour
%
12.0f
)
*
360.0f
/
12.0f
)
+
(
(
360.0f
/
12.0f
)
*
(
currentTime
.
Minute
%
60.0f
)
/
60.0f
)
+
(
(
360.0f
/
12.0f
)
*
(
currentTime
.
Second
%
60.0f
)
/
3600.0f
)
)
)
;

掌握原理之后,分针和秒针旋转角度的计算也是同理。
void
Update
(
)
{
//更新系统时间并转化为对应的指针旋转角度
currentTime
=
System
.
DateTime
.
Now
;
hourHand
.
transform
.
localRotation
=
UnityEngine
.
Quaternion
.
Euler
(
0.0f
,
0.0f
,
(
90.0f
+
(
(
currentTime
.
Hour
%
12.0f
)
*
360.0f
/
12.0f
)
+
(
(
360.0f
/
12.0f
)
*
(
currentTime
.
Minute
%
60.0f
)
/
60.0f
)
+
(
(
360.0f
/
12.0f
)
*
(
currentTime
.
Second
%
60.0f
)
/
3600.0f
)
)
)
;
minuteHand
.
transform
.
localRotation
=
UnityEngine
.
Quaternion
.
Euler
(
0.0f
,
0.0f
,
(
90.0f
+
(
(
currentTime
.
Minute
%
60.0f
)
*
360.0f
/
60.0f
)
+
(
(
360.0f
/
60.0f
)
*
(
currentTime
.
Second
%
60.0f
)
/
60.0f
)
)
)
;
secondHand
.
transform
.
localRotation
=
UnityEngine
.
Quaternion
.
Euler
(
(
270.0f
-
(
(
currentTime
.
Second
%
60.0f
)
*
360.0f
/
60.0f
)
)
,
90.0f
,
0.0f
)
;
}
写好脚本后,将时针、分针、秒针的GameObject分别拖入相应栏位。

现在运行游戏,闹钟将会显示为当前的系统时间!
三、UI制作
(一)显示界面
显示界面中,将显示当前闹铃时间,以及闹钟的设定按钮和停止按钮。
首先,在Hierarchy窗口中单击右键,选择UI>Canvas,新建画布。在Canvas下新建空物体,重命名为Group,用作显示界面各元素的父物体。
在Group下新建两个Button,分别命名为Set Ring和Stop Ring,将按钮文字分别改为“设定闹铃”和“停止闹铃”,并调整按钮至合适的位置和大小。
再在Group下新建两个Text,并排放置,给第二个Text重命名为Time,将Text的默认文字分别改为“当前闹铃时间:”和“未设定闹铃”,并调整文字的大小、颜色等参数。
制作好的显示界面UI如下所示:

(二)设定闹钟界面
新建一个空物体,命名为Setting Panel,将Transform归零。在Setting Panel下新建画布Canvas。在Canvas下再新建一个Panel,命名为Background,作为闹钟设定界面的背景图,并按需要调整大小、颜色和背景图片(Source Image)。本例中将这张背景图放在屏幕正中。

在Background下新建一个下拉列表(UI>Dropdown)和一个Text,二者并排放在背景图左上,将下拉列表重命名为Dropdown-Hour,将Text的文字改为“时”。点开下拉列表时,应显示24小时制的所有可选小时数,即0-23,所以我们需要在该下拉列表的Options中加入这些选项,如图。

设定分钟和秒的UI制作同理,只不过下拉列表的项数变为60项(0-59)。将分钟和秒的下拉列表分别重命名为“Dropdown - Minute”和“Dropdown - Second”。完成后的UI如下所示:

然后,在Background下新建两个按钮,分别重命名为“Confirm Button”和“Cancel Button”按钮文字分别为“确定”和“取消”。完成后,设定闹钟界面如下图所示:

最后,取消勾选Setting Panel,使之隐藏。
四、设定闹铃脚本
这部分脚本将存储设定的闹铃时间,并显示在UI中。
首先声明变量,为了方便区分,仍然建议在声明变量之前加个Header。
[
Header
(
"UI Elements"
)
]
public
GameObject
settingPanel
;
public
Button
settingRingBtn
;
public
Button
stopRingBtn
;
public
Button
confirmBtn
;
public
Button
cancelBtn
;
public
Dropdown
hourDropdown
;
public
Dropdown
minuteDropdown
;
public
Dropdown
secondDropdown
;
public
Text
timeText
;
int
hours
;
int
minutes
;
int
seconds
;
声明变量后,回到Unity中,为变量赋值。

编写以下脚本,其中包括三个方法,分别用来打开设定面板、设定闹铃时间和关闭设定面板。
//打开设定面板
void
OpenSettingPanel
(
)
{
settingPanel
.
SetActive
(
true
)
;
}
//设定闹铃时间
void
SettingRingTime
(
)
{
hours
=
hourDropdown
.
value
;
minutes
=
minuteDropdown
.
value
;
seconds
=
secondDropdown
.
value
;
timeText
.
text
=
hours
+
"时"
+
minutes
+
"分"
+
seconds
+
"秒"
;
settingPanel
.
SetActive
(
false
)
;
}
//关闭设定面板
void
CloseSettingPanel
(
)
{
settingPanel
.
SetActive
(
false
)
;
}
最后,在Start中为按钮添加Listener。
void
Start
(
)
{
settingRingBtn
.
onClick
.
AddListener
(
OpenSettingPanel
)
;
confirmBtn
.
onClick
.
AddListener
(
SettingRingTime
)
;
cancelBtn
.
onClick
.
AddListener
(
CloseSettingPanel
)
;
}
四、闹钟响铃与振动脚本
(一)闹钟响铃
先从网上找一段你喜欢的闹铃音效,保存在Project>Assets文件夹中。然后,在Clock脚本所附的闹钟物体上添加Audio Source组件,将音频拖进去,如图:

在Update语句块中,加入以下脚本:
void
Update
(
)
{
//如果系统时间等于设定时间,则闹钟响铃
if
(
currentTime
.
Hour
==
hours
&&
currentTime
.
Minute
==
minutes
&&
currentTime
.
Second
==
seconds
)
{
this
.
GetComponent
<
AudioSource
>
(
)
.
Play
(
)
;
}
}
(二)闹钟振动
在这个案例中,我们不用动画控制闹钟振动,而用代码来实现。这样做的好处是可以方便地调整闹钟振动的幅度、时间等参数。
和上面一样,首先声明闹钟振动相关变量。
[
Header
(
"Shaking Parameters"
)
]
public
float
time
=
3.0f
;
public
float
distance
=
0.1f
;
public
float
delayBetweenShakes
=
0f
;
private
Vector3
startPos
;
private
float
timer
;
private
Vector3
randomPos
;
因为闹钟“振动”这个动作需要一定时间,而且可能是反复进行的,所以适合用协程来写,如下:
//闹钟振动
public
void
StartShaking
(
)
{
StopAllCoroutines
(
)
;
StartCoroutine
(
Shake
(
)
)
;
}
private
IEnumerator
Shake
(
)
{
timer
=
0f
;
//time为闹钟振动的总时间
while
(
timer
<
time
)
{
timer
+=
Time
.
deltaTime
;
//生成随机位置,随机位置=初始位置+(单位半径球体内部随机点位置 * 距离乘数)
randomPos
=
startPos
+
(
UnityEngine
.
Random
.
insideUnitSphere
*
distance
)
;
//闹钟移动至该随机位置
transform
.
position
=
randomPos
;
//delayBetweenShakes表示每次振动之间的时间间隔,如为0则连续振动
if
(
delayBetweenShakes
>
0f
)
{
yield
return
new
WaitForSeconds
(
delayBetweenShakes
)
;
}
else
{
yield
return
null
;
}
}
//闹钟回到初始位置
transform
.
position
=
startPos
;
}
注意,time的值最好不长于闹铃音频的时长,否则就会出现闹钟已经不响,但仍然在振动的情况。
只要系统时间等于设定时间,就调用StartShaking方法,让闹钟开始振动。
void
Update
(
)
{
//如果系统时间等于设定时间,则闹钟开始振动
if
(
currentTime
.
Hour
==
hours
&&
currentTime
.
Minute
==
minutes
&&
currentTime
.
Second
==
seconds
)
{
StartShaking
(
)
;
}
}
最后,我们可能不想等待闹铃响完,这时点击之前制作的“停止闹铃”就可以关上它。为此,需要写一段停止闹铃的脚本,并在Start语句块中加入对按钮的Listener。
void
Start
(
)
{
stopRingBtn
.
onClick
.
AddListener
(
StopRing
)
;
}
void
StopRing
(
)
{
this
.
GetComponent
<
AudioSource
>
(
)
.
Stop
(
)
;
StopAllCoroutines
(
)
;
//由于中途结束了振动脚本,因此需要令闹钟回到初始位置
transform
.
position
=
startPos
;
}
附:本教程完整脚本
using
UnityEngine
;
using
System
;
using
UnityEngine
.
UI
;
using
System
.
Collections
;
public
class
Clock
:
MonoBehaviour
{
[
Header
(
"Clock Parts"
)
]
public
GameObject
hourHand
;
public
GameObject
minuteHand
;
public
GameObject
secondHand
;
DateTime
currentTime
;
[
Header
(
"UI Elements"
)
]
public
GameObject
settingPanel
;
public
Button
settingRingBtn
;
public
Button
stopRingBtn
;
public
Button
confirmBtn
;
public
Button
cancelBtn
;
public
Dropdown
hourDropdown
;
public
Dropdown
minuteDropdown
;
public
Dropdown
secondDropdown
;
public
Text
timeText
;
int
hours
;
int
minutes
;
int
seconds
;
[
Header
(
"Shaking Parameters"
)
]
public
float
time
=
0.2f
;
public
float
distance
=
0.1f
;
public
float
delayBetweenShakes
=
0f
;
private
Vector3
startPos
;
private
float
timer
;
private
Vector3
randomPos
;
// Start is called before the first frame update
void
Start
(
)
{
//记录闹钟初始位置
startPos
=
transform
.
position
;
settingRingBtn
.
onClick
.
AddListener
(
OpenSettingPanel
)
;
confirmBtn
.
onClick
.
AddListener
(
SettingRingTime
)
;
cancelBtn
.
onClick
.
AddListener
(
CloseSettingPanel
)
;
stopRingBtn
.
onClick
.
AddListener
(
StopRing
)
;
}
// Update is called once per frame
void
Update
(
)
{
//更新系统时间并转化为对应的指针旋转角度
currentTime
=
System
.
DateTime
.
Now
;
hourHand
.
transform
.
localRotation
=
UnityEngine
.
Quaternion
.
Euler
(
0.0f
,
0.0f
,
(
90.0f
+
(
(
currentTime
.
Hour
%
12.0f
)
*
360.0f
/
12.0f
)
+
(
(
360.0f
/
12.0f
)
*
(
currentTime
.
Minute
%
60.0f
)
/
60.0f
)
+
(
(
360.0f
/
12.0f
)
*
(
currentTime
.
Second
%
60.0f
)
/
3600.0f
)
)
)
;
minuteHand
.
transform
.
localRotation
=
UnityEngine
.
Quaternion
.
Euler
(
0.0f
,
0.0f
,
(
90.0f
+
(
(
currentTime
.
Minute
%
60.0f
)
*
360.0f
/
60.0f
)
+
(
(
360.0f
/
60.0f
)
*
(
currentTime
.
Second
%
60.0f
)
/
60.0f
)
)
)
;
secondHand
.
transform
.
localRotation
=
UnityEngine
.
Quaternion
.
Euler
(
(
270.0f
-
(
(
currentTime
.
Second
%
60.0f
)
*
360.0f
/
60.0f
)
)
,
90.0f
,
0.0f
)
;
//如果系统时间等于设定时间,则闹钟响铃
if
(
currentTime
.
Hour
==
hours
&&
currentTime
.
Minute
==
minutes
&&
currentTime
.
Second
==
seconds
)
{
this
.
GetComponent
<
AudioSource
>
(
)
.
Play
(
)
;
}
//如果系统时间等于设定时间,则闹钟开始振动
if
(
currentTime
.
Hour
==
hours
&&
currentTime
.
Minute
==
minutes
&&
currentTime
.
Second
==
seconds
)
{
StartShaking
(
)
;
}
}
//打开设定面板
void
OpenSettingPanel
(
)
{
settingPanel
.
SetActive
(
true
)
;
}
//设定闹铃时间
void
SettingRingTime
(
)
{
hours
=
hourDropdown
.
value
;
minutes
=
minuteDropdown
.
value
;
seconds
=
secondDropdown
.
value
;
timeText
.
text
=
hours
+
"时"
+
minutes
+
"分"
+
seconds
+
"秒"
;
settingPanel
.
SetActive
(
false
)
;
}
//关闭设定面板
void
CloseSettingPanel
(
)
{
settingPanel
.
SetActive
(
false
)
;
}
//停止闹铃
void
StopRing
(
)
{
this
.
GetComponent
<
AudioSource
>
(
)
.
Stop
(
)
;
StopAllCoroutines
(
)
;
//由于中途结束了振动脚本,因此需要令闹钟回到初始位置
transform
.
position
=
startPos
;
}
//闹钟振动
public
void
StartShaking
(
)
{
StopAllCoroutines
(
)
;
StartCoroutine
(
Shake
(
)
)
;
}
private
IEnumerator
Shake
(
)
{
timer
=
0f
;
//time为闹钟振动的总时间
while
(
timer
<
time
)
{
timer
+=
Time
.
deltaTime
;
//生成随机位置,随机位置=初始位置+(单位半径球体内部随机点位置 * 距离乘数)
randomPos
=
startPos
+
(
UnityEngine
.
Random
.
insideUnitSphere
*
distance
)
;
//闹钟移动至该随机位置
transform
.
position
=
randomPos
;
//delayBetweenShakes表示每次振动之间的时间间隔,如为0则连续振动
if
(
delayBetweenShakes
>
0f
)
{
yield
return
new
WaitForSeconds
(
delayBetweenShakes
)
;
}
else
{
yield
return
null
;
}
}
//闹钟回到初始位置
transform
.
position
=
startPos
;
}
}
现在,闹钟就制作完毕了。点击播放键来试试效果吧!