上一篇《iOS APP 架構漫談(一)》簡單介紹了information flow的概念。這篇文章簡單介紹另一個在程式設計中非常重要的思想或工具——狀态機(State machine)。 對大多數計算機專業的家夥們來說,這應該是一門比較難學的課程,裡面包含一大堆揪心的名字比如DFA,NFA,還有一大堆各種各樣的數學符号,又是編譯原理的基礎。不過很遺憾,似乎在做完編譯原理課程作業之後,很多人再也沒有實作過或是用過狀态機了。本文通過一個遊戲demo來簡單描述一下狀态機在實踐中的應用。demo code
背景
首先看下我們的使用場景,假如我們需要設計一套聯網對戰的小遊戲。第一個難題可能是如何建立一個通道,讓2個手機互相發送消息。這裡我并不打算引入server端開發,希望隻是通過用戶端來實作這個邏輯,這裡使用LeanCloud API來簡化這個過程。這樣我們可以暫時不考慮技術細節,直接站在業務角度去思考如何建立這個遊戲。
業務場景–邀請
正式開始遊戲之前,總會有一個邀請的環節。假如我們有2個使用者,分别是Host,Guest。Host建立遊戲,Guest加入遊戲。遊戲的整個流程和我們平時玩的對戰遊戲流程并沒有多大不同。
- Host建立遊戲,他就相當于進入一個等待隊列裡面。
- Guest加入遊戲,他從等待隊列中找到一個比對,比如Host。然後對Host發送join message
- Host會收到很多join message。由于我們隻是選擇1vs1。這裡假定Host同意Guest加入遊戲。Host向Guest發送join confirm message
- Guest收到join confirm message, 向Host發送Go消息,表示Guest已經進入遊戲
- Host收到Go消息。也進入遊戲。
具體實作業務邏輯
現在的構想的邏輯隻有5步,但其實還會包含很多邏輯,比如逾時機制,重發機制。由于中間狀态很多,還可能有我們沒有想到過的問題。在面對這種複雜邏輯時,會通過狀态機來幫助我們理順邏輯。這時,我們腦中思考的業務其實是一個狀态到一個狀态的圖。 如下
上半部分是遊戲的建立者,下半部分是遊戲的加入者。
一開始,盡量簡化模型,這裡紅色剪頭表示我們的正确主流路線,黑色表現出錯路線。也就是說,一旦錯誤,就回到原始Idle狀态。
開始寫代碼
在想清楚所有邏輯,并考慮清楚正常路線和錯誤路線之後,就可以開始寫代碼了。為了友善,這裡直接使用第三方的狀态機架構TransitionKit。
定義State(HOST)
TKState *idleState = [TKState stateWithName:@”idle”];
TKState *waitingJoinState = [TKState stateWithName:@”waitingJoin”];
TKState *waitingConfirmState = [TKState stateWithName:@”waitingConfirm”];
TKState *goState = [TKState stateWithName:@”go”];
[waitingConfirmState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
[selfWeak sendJoinConfirm];
}];
[goState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
NSLog(@”happy ending”);
[SVProgressHUD showSuccessWithStatus:@"ok"];
}];
定義Event(HOST)
Event 是建立State到State的路徑
TKEvent *waitingJoinEvent = [TKEvent eventWithName:CUHostGameManagerWaitingJoinEvent
transitioningFromStates:@[idleState]
toState:waitingJoinState];
TKEvent *receiveInviteEvent = [TKEvent eventWithName:CUHostGameManagerReceiveInviteEvent
transitioningFromStates:@[waitingJoinState]
toState:waitingConfirmState];
TKEvent *receiveConfirmEvent = [TKEvent eventWithName:CUHostGameManagerReceiveConfirmEvent
transitioningFromStates:@[waitingConfirmState]
toState:goState];
TKEvent *disconnectedEvent = [TKEvent eventWithName:CUHostGameManagerDisconnectedEvent
transitioningFromStates:nil
toState:idleState];
定義過程(HOST)
-
(void)startGame {
NSAssert(self.session.peerId != nil, @”“);
//這裡,如果不是idle,我們切換狀态機到idle
if (![self.stateMachine.currentState.name isEqual:@”idle”]) {
[self fireEvent:CUHostGameManagerDisconnectedEvent userInfo:nil];
}
//這裡調用LeanCloud 入隊
AVObject *waitingId = [AVObject objectWithClassName:@”waiting_join_Ids”];
[waitingId setObject:self.session.peerId forKey:@”peerId”];
[waitingId saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
//enqueue 之後,進入waitingJoin狀态
[self fireEvent:CUHostGameManagerWaitingJoinEvent userInfo:nil];
}];
}
-
(void)sendJoinConfirm {
//發送加入确認消息給Guest
AVMessage *message = [AVMessage messageForPeerWithSession:self.session
toPeerId:self.peerId
payload:@”join_confirm”];
[self.session sendMessage:message transient:YES];
}
-
(void)session:(AVSession )session didReceiveMessage:(AVMessage )message
{
if ([message.payload isEqualToString:@”join”]) {
//收到Join(邀請)之後,發送确認消息
self.peerId = message.fromPeerId;
//因為LeanCloud的API比較挫,watch 之後才能發送消息,但是我們不知道什麼時候才watch成功。。。。
//好在隻是demo,我們隻好用這種方式work around,延遲2s發送消息
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(sendInviteConfirmRequest:) object:nil];
[self performSelector:@selector(sendInviteConfirmRequest:)
withObject:@[message.fromPeerId]
afterDelay:2.0f];
}
else if ([message.payload isEqualToString:@”go”]) {
//收到go消息,流程結束
[self fireEvent:CUHostGameManagerReceiveConfirmEvent userInfo:nil];
}
}
-
(void)sendInviteConfirmRequest:(NSArray *)watchPeerIds {
[self.session watchPeerIds:watchPeerIds];
[self fireEvent:CUHostGameManagerReceiveInviteEvent userInfo:nil];
}
定義State(Guest)
TKState *idleState = [TKState stateWithName:@”idle”];
TKState *waitingReplyState = [TKState stateWithName:@”waitingReply”];
TKState *goState = [TKState stateWithName:@”go”];
[waitingReplyState setWillEnterStateBlock:^(TKState *state, TKTransition *transition) {
[selfWeak searchingGames];
}];
[goState setDidEnterStateBlock:^(TKState *state, TKTransition *transition) {
[selfWeak sendGo];
NSLog(@”happy ending”);
[SVProgressHUD showSuccessWithStatus:@”ok”];
}];
定義Event(Guest)
TKEvent *searchingEvent = [TKEvent eventWithName:CUGestGameManagerSearchingEvent
transitioningFromStates:@[idleState]
toState:waitingReplyState];
TKEvent *receiveConfirmEvent = [TKEvent eventWithName:CUGestGameManagerReceiveConfirmEvent
transitioningFromStates:@[waitingReplyState]
toState:goState];
TKEvent *disconnectedEvent = [TKEvent eventWithName:CUGestGameManagerDisconnectedEvent
transitioningFromStates:nil
toState:idleState];
定義過程(Guest)
- (void)joinGame {
if (![self.stateMachine.currentState.name isEqual:@”idle”]) {
[self fireEvent:CUGestGameManagerDisconnectedEvent userInfo:nil];
}
[self fireEvent:CUGestGameManagerSearchingEvent userInfo:nil];
}
-
(void)searchingGames {
AVQuery *query = [AVQuery queryWithClassName:@”waiting_join_Ids”];
[query orderByDescending:@”updatedAt”];
[query setLimit:1];
[query findObjectsInBackgroundWithBlock:^(NSArray *objects, NSError *error) {
NSMutableArray *installationIds = [[NSMutableArray alloc] init];
for (AVObject *object in objects) {
if ([object objectForKey:@”peerId”]) {
[installationIds addObject:[object objectForKey:@”peerId”]];
}
}
[self.session watchPeerIds:installationIds];
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(sendJoinRequest) object:nil];
[self performSelector:@selector(sendJoinRequest)
withObject:nil
afterDelay:2.0f];
}];
}
- (void)sendJoinRequest {
for (NSString *item in self.session.watchedPeerIds) {
AVMessage *message = [AVMessage messageForPeerWithSession:self.session
toPeerId:item
payload:@”join”];
[self.session sendMessage:message transient:YES];
}
}
-
(void)sendGo{
AVMessage *message = [AVMessage messageForPeerWithSession:self.session
toPeerId:self.otherPeerId
payload:@”go”];
[self.session sendMessage:message transient:YES];
}
最後
-
state machine 是一個蠻厲害的錘子,隻要是一個工具,就肯定會被濫用。。。state machine最大的好處是在于,友善我們思考清楚所有細節,主線,和錯誤流程。避免因為考慮不周全而産生的bug。結合之前的information flow的思路,會讓我們的軟體設計更加清楚。