貪喫蛇大作戰類遊戲的實現

貪喫蛇大作戰類遊戲的實現

前段時間玩了一個叫做貪喫蛇大作戰的手機遊戲,一下子就喜歡上了,然後就有了嘗試實現的想法。

製作的平臺環境:vs2012/cocos2dx3.8.1/C++

關於貪喫蛇遊戲的瞭解

記得在小時候玩的小遊戲機和老式手機就有了貪喫蛇遊戲,那時是一個格子一個格子的移動,就是不斷的喫食物。現在已經發展到了更加平滑的滑動,加入了擬人的ai來對抗,甚至還可以聯網一起競技,在我最近玩到的這個手遊,吸引我的大概就是流暢的操作和超更高分數努力以及和玩家競爭了。

期望實現的效果

最基本的是有像貪喫蛇大作戰一樣玩家控制一條蛇和其他ai一起搶食物變長,同時撞死對方,ai要比較真實,不會顯得太死板,隨着長度邊長遊戲仍能夠保證流暢性。至於聯網功能考慮了之後暫時放棄實現了。

系統設計

遊戲中的基本對象有兩類:

  1. 蛇,分玩家和ai;
  2. 食物,有多種類型,包括:隨機產生的食物,蛇死後的食物,隨機移動的星星;

實體(Entity)

所有對象都繼承自它,有唯一id,位置信息

蛇身

是蛇的每一節身體,繼承自Entity類,有一個Sprite

蛇的移動有軌跡性,後邊的身體都是沿着腦袋的軌跡來走的,同時蛇能夠加速,根據喫掉食物增加的分數能夠邊長和變粗。玩家的蛇完全由玩家輸入來控制,ai則檢測所處環境朝不同方向移動。

重要接口有:
void SetDir( Vec2 dir ); 設置要朝向的目標
void Rotate(); 會向朝向目標旋轉,蛇頭會有一個旋轉速度,每幀都會朝目標朝向轉,直到轉向完成。
void Move(); 蛇頭根據速度朝當前方向移動,更新蛇身路徑。
void MoveBodies(); 根據蛇身路徑移動蛇的身體。
void CheckEatFood(); 判斷是否喫到食物。
void CheckDie(); 判斷蛇是否撞到邊界和其他蛇的身體。
void UpdateNormalAI(); ai的更新函數,目的是確定當前的移動方向。
void ChangeBodySize(); 更新蛇身體大小和間隔。

這些函數基本上囊括了蛇的功能。

食物

食物包含三種:

  1. 地圖上隨機的食物,這是分數最小的,也是最多的,出現位置就不再變化;
  2. 蛇死後身體變成的食物,分數中等,出現後位置也不變;能夠吸引附近的ai加速搶喫。
  3. 地圖上隨機的星星,在地圖上一直移動的食物,分數最高,能夠吸引附近的ai加速搶喫。

同時蛇和食物的創建和管理都各自有一個管理器來管理,外部掌握的只是他們的id,外部獲取對應對象的時候,需要通過管理器來獲得指針,這樣防止了野指針出現的可能性,但是浪費了一些性能。

算法需求

稍微具有一些技術性的地方有兩個:
1. 蛇移動實現
2. 蛇ai實現

蛇身移動實現

參照貪喫蛇大作戰蛇身的移動效果,我做了如下實現方式:首先定義了蛇的基本移動單位,是一個常亮數值N,蛇的基本速度就是它;而蛇還有一個當前速度倍率Scale,代表當前蛇的速度,通過更改它的值來實現加速,在這個實現里加速我設爲2;蛇的移動路徑存Path儲爲一個list< Vec2 >,每次移動就根據當前的朝向在頭部插入Scale個路徑點,每個點間隔爲N;同時蛇身的間隔I是常量值N的倍數,它會根據蛇身長度增長和變大來變大,每幀每一個蛇身都會從Path的頭部開始,每間隔I個位置放置一個身體。部分實現代碼如下:

// Move()片段
for( int i = 1; i <= _curSpeedScale; i++ ) {
            _paths.push_front( _headPos + _dir * _initSpeed * i );
        }
    _headPos = _paths.front();
    MoveBodies();
    // 每隔常量秒,就刪除過長的路徑尾巴
    _clipPathDelta += dt;
    if( _clipPathDelta >= SNAKE_CLIP_PATH_INTERVAL ) {
        _clipPathDelta -= SNAKE_CLIP_PATH_INTERVAL;
        _paths.resize( 當前所需的路徑點數量 );
    }
}

ai的移動邏輯

最初設計的ai過於厲害,並且移動的非常假,雖然花費的很多邏輯實踐但是依然效果不行,後來簡化了實現,整體只考慮三個情景:
1. 首先判斷蛇首的警戒範圍是否有邊界或者別的蛇身,有的話設置目標方向爲相反方向,否則判斷第二種情況;
2. 判斷蛇首的視野範圍是否有星星類型食物,有的話設置當前朝向爲星星方向,並設置移動倍率爲2,否則判斷第三種情況;
3. 隨機朝某個位置移動;

如果每幀ai都在更新的話,蛇的反應速度依然會跟快,需要設置一個ai更新間隔,通過調整這個間隔和蛇首的警戒範圍來調整ai的反應速度,以能夠讓玩家有機會撞死ai。

優化

整體來說以上的步驟就已經實現了基本的貪喫蛇大作戰的ai玩法功能,但是實際運行起來發現,過一段時間遊戲就會非常的卡,每秒只達到30幀甚至20幀,做了一個性能分析功能(根據遊戲編程精粹1裏講解的一個實現)發現,在CheckDie(),UpdateNormalAi(),CheckEatFood()裏邊佔時很久,效果如下:
這裏寫圖片描述
這裏寫圖片描述

查看了一下代碼,發現在這些函數裏,都要獲得所有的食物或者蛇身整體遍歷來判斷是否進入範圍,這樣隨着蛇身越來越長食物越來越多遊戲會變得越來越卡。
我的解決辦法是將整個地圖分成多個區域,所有移動的蛇身和食物每次變換位置的時候就會更新他們所處的區域,某個蛇首判斷某範圍的食物時,只需要獲取該範圍的幾個區域裏邊的食物進行遍歷判斷就可以了,判斷死亡時也是隻取蛇首所在區域內的其他蛇身判斷距離。
實現該功能之後,發現在運行一會RefreshNodeRegion( entityType, entity* )函數浪費了太多時間,但是把代碼儘量優化之後仍然還是浪費過多時間,後來在與同事討論優化的時候,他提到可以通過vs2012分析工具來分析函數佔用cpu使用率,具體方法是通過工具欄的分析->分析嚮導來運行程序,一段時間後會計算出各個函數使用情況,發現使用最多的果然是在這個函數中,進入之後發現個這個函數每行代碼的佔用率,發現了問題所在,原代碼如下:

int id = entity->GetId();
CCAssert(  _entities.find( id ) != _entities.end(), "" );

std::vector<Vec2> oldRegions = _entities[id];
_entities[id].clear();
GetCoverRegion( entity->GetPosition(), entity->GetRadius(), _entities[id] );

auto oItr = oldRegions.begin(), nItr = _entities[id].begin();
while( oItr != oldRegions.end() ) {
    RemoveNodeInRegion( type, id, (*oItr).y, (*oItr).x );
    ++oItr;
}
while( nItr != _entities[id].end() ) {
    AddNodeInRegion( type, id, (*nItr).y, (*nItr).x );
    ++nItr;
}

由於_entities是一個 std::map< int, std::vector< Vec2 > >, 每次使用_entities[id]都會導致重新搜尋一遍_entities找到對應id的信息,優化後改成了這樣:

int id = entity->GetId();
auto itr = _entities.find( id );
CCAssert(  itr != _entities.end(), "" );

std::vector<Vec2> oldRegions( itr->second );
itr->second.clear();
GetCoverRegion( entity->GetPosition(), entity->GetRadius(), _entities[id] );

auto oItr = oldRegions.begin(), nItr = itr->second.begin();
while( oItr != oldRegions.end() ) {
    RemoveNodeInRegion( type, id, (*oItr).y, (*oItr).x );
    ++oItr;
}
    while( nItr != itr->second.end() ) {
    AddNodeInRegion( type, id, (*nItr).y, (*nItr).x );
    ++nItr;
}

關閉所有打印信息,重新運行,遊戲基本上能夠50幀以上流暢運行,至此該遊戲的實現終於完成了, 通過這個遊戲的實現過程我也更加了解了遊戲優化的一些方法!

遊戲運行demo和源碼在此下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章