上篇给大家介绍了一个自定义的简单的使用switch语句实现的有限状态机,这次给大家介绍下一个简单的FSM框架,
使用简单的FSM框架设计,可以避免我们使用switch实现的代码冗余,我们可以做到一个状态一个类,高效直观的区分开来。这个框架可以在unitycommunity.com找到 ,地址是http://wiki.unity3d.com/index.php?title=Finite_State_Machine
首先我们还是使用昨天用过的例子来制作,
图表显示了怪物AI 的一些列状态,在框架实现方式中,表里面每一个“当前状态”都对应着一个状态类。
比如 “巡逻”状态对应一个PatrolState类,“追逐”状态对应着ChaseState类,等。。。 这些状态类都继承同一个父类 FSMState类。
每个状态类都拥有自己的“字典”,里面存储着当前状态所拥有的“转换--新状态”对,表明这个状态下,如果发生某个“转换”事件,
即表中的“输入”,状态机就会转移到何种的新状态。
例如,在PatrolState类中,字典会有一个SawPlayer-Chase的项,表明在巡逻状态PatrolState下,如果看到玩家SawPlayer,
那么转换到追逐状态ChaseState。因此,可以把“输入” 看做是字典的关键字,而转移得到的新状态可以认为是根据关键字查询
字典得到的结果。
另外,FSM框架中还有一个重要的类---AdvancedFSM类,它负责管理所有这些状态类如 巡逻,追逐,攻击 等。
好,下面我们开始看看具体的实现,首先,FSMState 类是所有状态类的基类,它的每个派生类都代表了FSM中的某个状态,
状态类中具有添加转换,删除转换的方法,用于管理记录这些转换。
在FSMState类中,包含了一个字典对象,称为Map,可以在类中的AddTransition方法和DeleteTransition方法添加或删除“转换-状态”对。
这个类还包括Reason方法和Act方法。其中,Reason方法用来确定是否需要转换到其他状态,应该发生哪个转换;
Act方法定义了在本状态的角色行为,比如移动,播放动画等。
下面是FSMState 的代码
然后是AdvancedFSM类,它是FSM类的派生类,负责管理FSMState的派生类,并且随着当前状态和输入,进行状态更新,
需要注意的是这个类不能有Start(),Update(),和FixedUpdate()函数,否则将会覆盖基类的函数
手码了好多注释,累
在这种视线中,一个AdvanceFSM类可以管理和使用任意数目的FSMState,两个类共同为用户提供了一个通用的FSM框架,
它们能够支持多种状态,多种FSM输入以及多种状态转移。
然后下面我们开始说其他的状态类实现,和上篇的例子不同,在这个FSM框架中,AI角色的状态在不同的类里面实现,
且都是FSMState的派生类,每个类都需要实现Reason和Act方法
代码里面我把播放动画之类的注释了,为能简单演示一下FSM框架的强大,
下面是巡逻类代码
然后是追逐类
攻击类
还是死亡类
最后是一个控制类AIController 负责创建有限状态机,通过它控制AI角色
好了,代码写完了,下面我们来布置场景,和上篇类似,不同的地方就是EnemyAI不用SimpleFSM,而更换为AIController。
其他都一样,然后运行 我们就可以看见一个AI在地图中巡逻,我们玩家上前 AI就会主动追过来,然后攻击,然后死亡。
最后我们也上传一下源代码,里面有2个场景,1就是上篇的代码,2就是本次的代码。
FSM状态机就简单介绍到这里,下次学到什么再和大家分享.谢谢大家。

图表显示了怪物AI 的一些列状态,在框架实现方式中,表里面每一个“当前状态”都对应着一个状态类。
比如 “巡逻”状态对应一个PatrolState类,“追逐”状态对应着ChaseState类,等。。。 这些状态类都继承同一个父类 FSMState类。
每个状态类都拥有自己的“字典”,里面存储着当前状态所拥有的“转换--新状态”对,表明这个状态下,如果发生某个“转换”事件,
即表中的“输入”,状态机就会转移到何种的新状态。
例如,在PatrolState类中,字典会有一个SawPlayer-Chase的项,表明在巡逻状态PatrolState下,如果看到玩家SawPlayer,
那么转换到追逐状态ChaseState。因此,可以把“输入” 看做是字典的关键字,而转移得到的新状态可以认为是根据关键字查询
字典得到的结果。
另外,FSM框架中还有一个重要的类---AdvancedFSM类,它负责管理所有这些状态类如 巡逻,追逐,攻击 等。
好,下面我们开始看看具体的实现,首先,FSMState 类是所有状态类的基类,它的每个派生类都代表了FSM中的某个状态,
状态类中具有添加转换,删除转换的方法,用于管理记录这些转换。
在FSMState类中,包含了一个字典对象,称为Map,可以在类中的AddTransition方法和DeleteTransition方法添加或删除“转换-状态”对。
这个类还包括Reason方法和Act方法。其中,Reason方法用来确定是否需要转换到其他状态,应该发生哪个转换;
Act方法定义了在本状态的角色行为,比如移动,播放动画等。
下面是FSMState 的代码
[C#]
纯文本查看
复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
[/b][/size]
[size=4][b]
using
UnityEngine;
using
System.Collections;
using
System.Collections.Generic;
/// <summary>
/// This class is adapted and modified from the FSM implementation class available on UnifyCommunity website
/// The license for the code is Creative Commons Attribution Share Alike.
/// It's originally the port of C++ FSM implementation mentioned in Chapter01 of Game Programming Gems 1
/// You're free to use, modify and distribute the code in any projects including commercial ones.
/// Please read the link to know more about CCA license @http://creativecommons.org/licenses/by-sa/3.0/
/// This class represents the States in the Finite State System.
/// Each state has a Dictionary with pairs (transition-state) showing
/// which state the FSM should be if a transition is fired while this state
/// is the current state.
/// Reason method is used to determine which transition should be fired .
/// Act method has the code to perform the actions the NPC is supposed to do if it磗 on this state.
/// </summary>
public
abstract
class
FSMState
{
//字典,字典中每一项都记录了一个“转换-状态”对 的信息
protected
Dictionary<Transition, FSMStateID> map =
new
Dictionary<Transition, FSMStateID>();
//状态编号ID
protected
FSMStateID stateID;
public
FSMStateID ID {
get
{
return
stateID; } }
//下面需要用到的,与各状态相关的变量
//目标点的位置
protected
Vector3 destPos;
//巡逻点的数组,存储巡逻要经过的点
protected
Transform[] waypoints;
//转向速度
protected
float
curRotSpeed;
//移动速度
protected
float
curSpeed;
//AI与玩家距离小于该值,开始追逐
protected
float
chaseDistance = 40.0f;
//小于这个就攻击
protected
float
attackDistance = 20.0f;
//巡逻小于这值,就认为到达巡逻点了
protected
float
arriveDistance = 3.0f;
/// <summary>
/// 向字典添加项,每项是一个"转换--状态"对
/// </summary>
/// <param name="transition"></param>
/// <param name="id"></param>
public
void
AddTransition(Transition transition, FSMStateID id)
{
//检查这个转换(可以看做是字典的关键字)是否在字典里
if
(map.ContainsKey(transition))
{
//一个转换只能对应一个新状态
Debug.LogWarning(
"FSMState ERROR: transition is already inside the map"
);
return
;
}
//如果不在字典,那么将这个转换和转换后的状态作为一个新的字典项,加入字典
map.Add(transition, id);
Debug.Log(
"Added : "
+ transition +
" with ID : "
+ id);
}
/// <summary>
/// 从字典中删除项
/// </summary>
/// <param name="trans"></param>
public
void
DeleteTransition(Transition trans)
{
// 检查是否在字典中,如果在,移除
if
(map.ContainsKey(trans))
{
map.Remove(trans);
return
;
}
//如果要删除的项不在字典中,报告错误
Debug.LogError(
"FSMState ERROR: Transition passed was not on this State List"
);
}
/// <summary>
/// 通过查询字典,确定在当前状态下,发生trans转换时,应该转换到新的状态编号并返回
/// </summary>
/// <param name="trans"></param>
/// <returns></returns>
public
FSMStateID GetOutputState(Transition trans)
{
return
map[trans];
}
/// <summary>
/// 用来确定是否需要转换到其他状态,应该发生哪个转换
/// </summary>
/// <param name="player"></param>
/// <param name="npc"></param>
public
abstract
void
Reason(Transform player, Transform npc);
/// <summary>
/// 定义了在本状态的角色行为,移动,动画等
/// </summary>
/// <param name="player"></param>
/// <param name="npc"></param>
public
abstract
void
Act(Transform player, Transform npc);
/// <summary>
/// 寻找下一个巡逻点
/// </summary>
public
void
FindNextPoint()
{
//Debug.Log("Finding next point");
int
rndIndex = Random.Range(0, waypoints.Length);
Vector3 rndPosition = Vector3.zero;
destPos = waypoints[rndIndex].position + rndPosition;
}
/// <summary>
/// Check whether the next random position is the same as current tank position
/// </summary>
/// <param name="pos">position to check</param>
/*
protected bool IsInCurrentRange(Transform trans, Vector3 pos)
{
float xPos = Mathf.Abs(pos.x - trans.position.x);
float zPos = Mathf.Abs(pos.z - trans.position.z);
if (xPos <= 50 && zPos <= 50)
return true;
return false;
}*/
}[b]
[/b]
|
然后是AdvancedFSM类,它是FSM类的派生类,负责管理FSMState的派生类,并且随着当前状态和输入,进行状态更新,
需要注意的是这个类不能有Start(),Update(),和FixedUpdate()函数,否则将会覆盖基类的函数
[C#]
纯文本查看
复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
using
UnityEngine;
using
System.Collections;
using
System.Collections.Generic;
/// <summary>
/// This class is adapted and modified from the FSM implementation class available on UnifyCommunity website
/// The license for the code is Creative Commons Attribution Share Alike.
/// It's originally the port of C++ FSM implementation mentioned in Chapter01 of Game Programming Gems 1
/// You're free to use, modify and distribute the code in any projects including commercial ones.
/// Please read the link to know more about CCA license @http://creativecommons.org/licenses/by-sa/3.0/
/// </summary>
//定义枚举,为可能的转换分配编号
public
enum
Transition
{
SawPlayer = 0,
//看到玩家
ReachPlayer,
//接近玩家
LostPlayer,
//玩家离开视线
NoHealth,
//死亡
}
/// <summary>
/// 定义枚举,为可能的状态分配编号ID
/// </summary>
public
enum
FSMStateID
{
Patrolling = 0,
//巡逻编号
Chasing,
//追逐编号
Attacking,
//攻击编号
Dead,
//死亡编号
}
public
class
AdvancedFSM : FSM
{
//FSM中的所有状态组成的列表
private
List<FSMState> fsmStates;
//当前状态的编号
//The fsmStates are not changing directly but updated by using transitions
private
FSMStateID currentStateID;
public
FSMStateID CurrentStateID {
get
{
return
currentStateID; } }
//当前状态
private
FSMState currentState;
public
FSMState CurrentState {
get
{
return
currentState; } }
public
AdvancedFSM()
{
//新建一个空的状态列表
fsmStates =
new
List<FSMState>();
}
/// <summary>
///向状态列表中加入一个新的状态
/// </summary>
public
void
AddFSMState(FSMState fsmState)
{
//检查要加入的新状态是否为空,如果空就报错
if
(fsmState ==
null
)
{
Debug.LogError(
"FSM ERROR: Null reference is not allowed"
);
}
// First State inserted is also the Initial state
// the state the machine is in when the simulation begins
//如果插入的这个状态时,列表还是空的,那么将它加入列表并返回
if
(fsmStates.Count == 0)
{
fsmStates.Add(fsmState);
currentState = fsmState;
currentStateID = fsmState.ID;
return
;
}
// 检查要加入的状态是否已经在列表里,如果是,报错返回
foreach
(FSMState state
in
fsmStates)
{
if
(state.ID == fsmState.ID)
{
Debug.LogError(
"FSM ERROR: Trying to add a state that was already inside the list"
);
return
;
}
}
//如果要加入的状态不在列表中,将它加入列表
fsmStates.Add(fsmState);
}
//从状态中删除一个状态
public
void
DeleteState(FSMStateID fsmState)
{
// 搜索整个状态列表,如果要删除的状态在列表中,那么将它移除,否则报错
foreach
(FSMState state
in
fsmStates)
{
if
(state.ID == fsmState)
{
fsmStates.Remove(state);
return
;
}
}
Debug.LogError(
"FSM ERROR: The state passed was not on the list. Impossible to delete it"
);
}
/// <summary>
/// 根据当前状态,和参数中传递的转换,转换到新状态
/// </summary>
public
void
PerformTransition(Transition trans)
{
// 根绝当前的状态类,以Trans为参数调用它的GetOutputState方法
//确定转换后的新状态
FSMStateID id = currentState.GetOutputState(trans);
// 将当前状态编号设置为刚刚返回的新状态编号
currentStateID = id;
//根绝状态编号查找状态列表,将当前状态设置为查找到的状态
foreach
(FSMState state
in
fsmStates)
{
if
(state.ID == currentStateID)
{
currentState = state;
break
;
}
}
}
}
|
手码了好多注释,累
在这种视线中,一个AdvanceFSM类可以管理和使用任意数目的FSMState,两个类共同为用户提供了一个通用的FSM框架,
它们能够支持多种状态,多种FSM输入以及多种状态转移。
然后下面我们开始说其他的状态类实现,和上篇的例子不同,在这个FSM框架中,AI角色的状态在不同的类里面实现,
且都是FSMState的派生类,每个类都需要实现Reason和Act方法
代码里面我把播放动画之类的注释了,为能简单演示一下FSM框架的强大,
下面是巡逻类代码
[C#]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
using
UnityEngine;
using
System.Collections;
public
class
PatrolState : FSMState
{
public
PatrolState(Transform[] wp)
{
waypoints = wp;
stateID = FSMStateID.Patrolling;
curRotSpeed = 6.0f;
curSpeed = 80.0f;
}
public
override
void
Reason(Transform player, Transform npc)
{
//Check the distance with player tank
//When the distance is near, transition to chase state
if
(Vector3.Distance(npc.position, player.position) <= chaseDistance)
{
Debug.Log(
"Switch to Chase State"
);
npc.GetComponent<AIController>().SetTransition(Transition.SawPlayer);
}
}
public
override
void
Act(Transform player, Transform npc)
{
//Find another random patrol point if the current point is reached
if
(Vector3.Distance(npc.position, destPos) <= arriveDistance)
{
Debug.Log(
"Reached to the destination point\ncalculating the next point"
);
FindNextPoint();
}
//Rotate to the target point
Quaternion targetRotation = Quaternion.LookRotation(destPos - npc.position);
npc.rotation = Quaternion.Slerp(npc.rotation, targetRotation, Time.deltaTime * curRotSpeed);
//Go Forward
CharacterController controller = npc.GetComponent<CharacterController>();
controller.SimpleMove(npc.transform.forward * Time.deltaTime * curSpeed);
Animation animComponent = npc.GetComponent<Animation>();
//animComponent.CrossFade("Walk");
}
}
|
然后是追逐类
[C#]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
using
UnityEngine;
using
System.Collections;
public
class
ChaseState : FSMState
{
public
ChaseState(Transform[] wp)
{
waypoints = wp;
stateID = FSMStateID.Chasing;
curRotSpeed = 6.0f;
curSpeed = 160.0f;
//find next Waypoint position
FindNextPoint();
}
public
override
void
Reason(Transform player, Transform npc)
{
//Set the target position as the player position
destPos = player.position;
//Check the distance with player tank
//When the distance is near, transition to attack state
float
dist = Vector3.Distance(npc.position, destPos);
if
(dist <= attackDistance)
{
Debug.Log(
"Switch to Attack state"
);
npc.GetComponent<AIController>().SetTransition(Transition.ReachPlayer);
}
//Go back to patrol is it become too far
else
if
(dist >= chaseDistance)
{
Debug.Log(
"Switch to Patrol state"
);
npc.GetComponent<AIController>().SetTransition(Transition.LostPlayer);
}
}
public
override
void
Act(Transform player, Transform npc)
{
//Rotate to the target point
destPos = player.position;
Quaternion targetRotation = Quaternion.LookRotation(destPos - npc.position);
npc.rotation = Quaternion.Slerp(npc.rotation, targetRotation, Time.deltaTime * curRotSpeed);
//Go Forward
//npc.Translate(Vector3.forward * Time.deltaTime * curSpeed);
CharacterController controller = npc.GetComponent<CharacterController>();
controller.SimpleMove(npc.transform.forward * Time.deltaTime * curSpeed);
Animation animComponent = npc.GetComponent<Animation>();
//animComponent.CrossFade("Run");
}
}
|
攻击类
[C#]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
using
UnityEngine;
using
System.Collections;
public
class
AttackState : FSMState
{
public
AttackState(Transform[] wp)
{
waypoints = wp;
stateID = FSMStateID.Attacking;
curRotSpeed = 12.0f;
curSpeed = 100.0f;
//find next Waypoint position
FindNextPoint();
}
public
override
void
Reason(Transform player, Transform npc)
{
//Check the distance with the player tank
float
dist = Vector3.Distance(npc.position, player.position);
if
(dist >= attackDistance && dist < chaseDistance)
{
Debug.Log(
"Switch to Chase State"
);
npc.GetComponent<AIController>().SetTransition(Transition.SawPlayer);
}
//Transition to patrol is the tank become too far
else
if
(dist >= chaseDistance)
{
Debug.Log(
"Switch to Patrol State"
);
npc.GetComponent<AIController>().SetTransition(Transition.LostPlayer);
}
}
public
override
void
Act(Transform player, Transform npc)
{
//Set the target position as the player position
destPos = player.position;
//Rotate to the target point
Quaternion targetRotation = Quaternion.LookRotation(destPos - npc.position);
npc.rotation = Quaternion.Slerp(npc.rotation, targetRotation, Time.deltaTime * curRotSpeed);
//Shoot bullet towards the player
Animation animComponent = npc.GetComponent<Animation>();
//animComponent.CrossFade("StandingFire");
npc.GetComponent<AIController>().ShootBullet();
}
}
|
还是死亡类
[C#]
纯文本查看
复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
using
UnityEngine;
using
System.Collections;
public
class
DeadState : FSMState
{
public
DeadState()
{
stateID = FSMStateID.Dead;
}
public
override
void
Reason(Transform player, Transform npc)
{
}
public
override
void
Act(Transform player, Transform npc)
{
Animation animComponent = npc.GetComponent<Animation>();
//animComponent.CrossFade("death");
}
}
|
最后是一个控制类AIController 负责创建有限状态机,通过它控制AI角色
[C#]
纯文本查看
复制代码
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
|
using
UnityEngine;
using
System.Collections;
public
class
AIController : AdvancedFSM
{
public
GameObject Bullet;
public
Transform bulletSpawnPoint;
private
int
health;
//Initialize the Finite state machine for the NPC tank
protected
override
void
Initialize()
{
health = 100;
elapsedTime = 0.0f;
shootRate = 0.5f;
//Get the target enemy(Player)
GameObject objPlayer = GameObject.FindGameObjectWithTag(
"Player"
);
playerTransform = objPlayer.transform;
if
(!playerTransform)
print(
"Player doesn't exist.. Please add one with Tag named 'Player'"
);
//Start Doing the Finite State Machine
ConstructFSM();
}
//Update each frame
protected
override
void
FSMUpdate()
{
//Check for health
elapsedTime += Time.deltaTime;
}
protected
override
void
FSMFixedUpdate()
{
CurrentState.Reason(playerTransform, transform);
CurrentState.Act(playerTransform, transform);
}
public
void
SetTransition(Transition t)
{
PerformTransition(t);
}
private
void
ConstructFSM()
{
//Get the list of points
pointList = GameObject.FindGameObjectsWithTag(
"PatrolPoint"
);
Transform[] waypoints =
new
Transform[pointList.Length];
int
i = 0;
foreach
(GameObject obj
in
pointList)
{
waypoints[i] = obj.transform;
i++;
}
PatrolState patrol =
new
PatrolState(waypoints);
patrol.AddTransition(Transition.SawPlayer, FSMStateID.Chasing);
patrol.AddTransition(Transition.NoHealth, FSMStateID.Dead);
ChaseState chase =
new
ChaseState(waypoints);
chase.AddTransition(Transition.LostPlayer, FSMStateID.Patrolling);
chase.AddTransition(Transition.ReachPlayer, FSMStateID.Attacking);
chase.AddTransition(Transition.NoHealth, FSMStateID.Dead);
AttackState attack =
new
AttackState(waypoints);
attack.AddTransition(Transition.LostPlayer, FSMStateID.Patrolling);
attack.AddTransition(Transition.SawPlayer, FSMStateID.Chasing);
attack.AddTransition(Transition.NoHealth, FSMStateID.Dead);
DeadState dead =
new
DeadState();
dead.AddTransition(Transition.NoHealth, FSMStateID.Dead);
AddFSMState(patrol);
AddFSMState(chase);
AddFSMState(attack);
AddFSMState(dead);
}
/// <summary>
/// Check the collision with the bullet
/// </summary>
/// <param name="collision"></param>
void
OnCollisionEnter(Collision collision)
{
//Reduce health
if
(collision.gameObject.tag ==
"Bullet"
)
{
health -= 50;
if
(health <= 0)
{
Debug.Log(
"Switch to Dead State"
);
SetTransition(Transition.NoHealth);
}
}
}
// Shoot the bullet
public
void
ShootBullet()
{
if
(elapsedTime >= shootRate)
{
GameObject bulletObj = Instantiate(Bullet, bulletSpawnPoint.position, transform.rotation)
as
GameObject;
bulletObj.GetComponent<Bullet>().Go();
elapsedTime = 0.0f;
}
}
}
|
好了,代码写完了,下面我们来布置场景,和上篇类似,不同的地方就是EnemyAI不用SimpleFSM,而更换为AIController。
其他都一样,然后运行 我们就可以看见一个AI在地图中巡逻,我们玩家上前 AI就会主动追过来,然后攻击,然后死亡。

最后我们也上传一下源代码,里面有2个场景,1就是上篇的代码,2就是本次的代码。
FSM状态机就简单介绍到这里,下次学到什么再和大家分享.谢谢大家。