目標
本章最後會寫一個回合制的 RPG 遊戲,怪物是個物件,玩家是個物件,整個世界地圖也是個物件。
物件 (Object)
物件 (Object) 說白了就是把資料 (data) 和函式 (function / method) 包在一起的概念。請參考下面的程式:
counter.hpp
#ifndef COUNTER_HEADER
#define COUNTER_HEADER
class Counter
{
public:
Counter()
{
count_ = 0;
}
Counter( int init_count )
{
count_ = init_count;
}
~Counter() { }
void inc()
{
count_ ++;
}
int count_;
};
#endif
main.cpp
#include <iostream>
#include "counter.hpp"
int main()
{
Counter counter_1, counter_2( 5 ); // constructor
Counter *counter_3;
counter_3 = new Counter(); // constructor
counter_1.inc();
counter_2.inc();
counter_3->inc(); // convenient operator "->"
(*counter_3).inc(); // same meaning as above
std::cout << counter_1.count_ << std::endl;
std::cout << counter_2.count_ << std::endl;
std::cout << counter_3->count_ << std::endl;
delete counter_3; // destructor
// counter_1/counter_2's destructor called here
}
- public 指的是 method 和 data 都開放給別人隨便使用
- 因為我們不希望 counter_ 被別人隨便改,所以應該改為 private
- 約定俗成的看法是所有的 data 都要宣告為 private
- Constructor (建構子) 在物件被建立時會被呼叫 (stack vs heap)
- Destructor (解構子) 在物件消失時會被呼叫 (stack vs heap)
- 便利的 operator ->
Namespace
為了怕撞名,C++有命名空間 (namespace) 的語法,拿 counter 這個物件當例子,我們給他一個命名空間叫 superlib。
#ifndef COUNTER_HEADER
#define COUNTER_HEADER
namespace superlib
{
class Counter;
}
class superlib::Counter
{
public:
Counter() { count_ = 0; }
~Counter() { }
inc() { count_ ++; }
private:
int count_;
};
#endif
在使用時,原來叫 Counter 的就改叫 superlib::Counter就好。命名空間也可以直接把整個宣告用 namespace 框起來,下一小節的例子就是。
這小節的目的只為了是說明 std:: 的字頭代表的意義,std 是 C++ 標準庫的 namespace。但是在單人作業的小型的 project 中用 namespace 只是找自己麻煩,會用並不代表需要使用,程式精簡易讀比看起來酷炫帥氣重要。:-)
Header for Class
物件的宣告和實作也可以分成二個檔案來處理。
counter.hpp
#ifndef COUNTER_HEADER
#define COUNTER_HEADER
namespace superlib
{
class Counter
{
public:
Counter();
~Counter();
void inc();
private:
int count_;
};
}
#endif
counter.cpp
#include "counter.hpp"
superlib::Counter::Counter()
{
counter_ = 0;
}
superlib::Counter::Counter( int init )
{
counter_ = init;
}
superlib::Counter::~Counter()
{
}
void superlib::Counter::inc()
{
counter_++;
}
main.cpp
#include "counter.hpp"
int main()
{
superlib::Counter counter;
counter.inc();
return 0;
}
Standard C++ Library
標準的 C++ Library 就是已經寫好的物件,或叫函式庫。這些標準的函示庫都是在 std namespace 下。我們介紹二個這章會用的標準函式庫。
C++ String
不同於 C String 只是個 char array,C++ String 是一個包括了資料和字串操作的物件。我們直接把這章會用的功能寫成一個範例。
#include <iostream>
#include <string>
int main()
{
std::string str1, str2( "my str2" ), str3;
std::string substr;
str1 = "hello";
str3 = str1 + str2;
substr = str2.substr(3, 4);
std::cout << "str1.size(): " << str1.size() << std::endl;
std::cout << "str2[1]: " << str2[1] << std::endl;
std::cout << "str2.substr(3, 4): " << substr << std::endl;
return 0;
}
- 我們用了 substr 的 method 取得並複製了一部分的 str2 的字串
- std::string 有多種 construtor,我們用了二種
- std::string 有複寫了 =,[],+ 等多種的 operator,這些進階的功能這邊不會提及要如何做到,但是如果想要,我們也可以讓前面的 Class Counter 也能做到 counter3 = count1+ count2 或是 count1[4] = 5 之類操作,但複寫這些 operator 對 Class Counter 有沒有意義就是另一回事了。
C++ Vector
C++ 有一整套跟資料結構相關的函式庫,有個專有名詞叫 STL (Standard Template Library) ,裡面用到了 C++ 很獨特的 Template 語法。老實說,Template 不易寫,很複雜,出錯也很難 debug,複雜度的問題一直都是 C++ 被人攻擊的點。Template 的優點是速度很快,因為它是直接在 compile 時依照資料類型產生代碼,而非 runtime 來處理不同類型的資料的 casting。
資料結構就是存放資料的方法,我們介紹最常用的 Vector。
Vector 是一個會自己長大的陣列,舉例來說,一開始先預留 10 個整數,如果使用者一直用,用到了超過 10 的位置,Vector 就會自動從 heap 再要一個更大的記憶體,然後把舊的只有 10 個整數的陣列複製過去,然後繼續使用。所以使用 Vector 時我們就不用怕陣列太小不夠用了。
Vector 的優點是存取中間某個資料的速度較快,缺點是要刪除中間的資料要花更多時間,和另一種叫 List 的資料結構正好相反。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> int_array;
std::vector<int>::iterator it_int;
// change capacity
std::cout << "capacity:" << int_array.capacity() << std::endl;
int_array.reserve( 10 );
std::cout << "capacity:" << int_array.capacity() << std::endl;
// change size
int_array.resize( 10 );
// assign 10 value to int_array[0~9]
for ( int i = 0; i < 10; i ++ )
{
int_array[i] = i;
}
std::cout << "size:" << int_array.size() << std::endl;
// find and delete
iterator = int_array.begin(); // iterator points to array[0]
iterator ++; // iterator points to array[1]
int_array.erase( iterator ); // delete array[1]
std::cout << "size: " << int_array.size() << std::endl;
// traversal
for (auto it = int_array.begin(); it != int_array.end(); it++)
{
std::cout << *it << std::endl;
}
return 0;
}
- 了解 vector / link / map 的大致概念
- 了解 STL 的 iterator 設定 (用法不是很直覺,需要記一下)
- 了解 C++11 的 auto iterator (convenient syntax)
繼承 (Inheritance)
我們直接用怪物和玩家的物件來說明繼承是什麼。
在地圖上,每回合玩家會動,怪物也會動。對遊戲來說,玩家和怪物在同樣 “會動” 的概念下是同類型。差別是玩家怎麼動是靠玩家自己輸入的,怪物怎麼動是遊戲邏輯決定的。
我們先寫一個叫 Moveable 的物件,裡面有個 virtual function 叫 next_move,可以回傳 1~4 分別代表 N\E\S\W。virtual 的意思是告訴 compiler 這個 function 是等等會被 (或說被允許) 複寫掉的。
然後再寫二個物件,一個叫 monster,它繼承了 moveable,複寫掉 next_move 並用 override 的贅詞告訴 compiler 複寫這件事 (主要目的只是防呆除錯)。一個叫 player,它也繼承同樣的 moveable,然後用 std::cin 取得下一步要去哪的指令。
mob.hpp
#ifndef MOB_HPP
#define MOB_HPP
#include <iostream>
class Moveable
{
public:
virtual int type() { return 0; }
virtual int next_move() { return 0; }
};
class Monster : public Moveable
{
public:
int type() override { return 10; }
int next_move() override
{
return 1; // not-very-smart monster that can only go north
}
};
class Player : public Moveable
{
public:
int type() override { return 20; }
int next_move() override
{
int move;
std::cout << "your next move? (1~4) ";
std::cin >> move;
return move;
}
};
#endif
然後我們看一下 main.cpp,我們建了一個 Moveable* 的 vector 陣列,裡面同時放了 Monster 和 Player。因為 Monster 和 Player 都是 Moveable 型態的,所以不會有 compile error。然後因為 Monster 和 Player 都有 type() 和 next_move() 的 function,所以可以無違和的當同樣的物件來使用。
#include <iostream>
#include <vector>
#include "mob.hpp"
int main()
{
std::vector<Moveable*> mobs;
mobs.push_back( new Monster() );
mobs.push_back( new Monster() );
mobs.push_back( new Monster() );
mobs.push_back( new Monster() );
mobs.push_back( new Player() );
while ( true )
{
for ( auto it = mobs.begin(); it != mobs.end(); it ++ )
{
int move = (*it)->next_move();
std::cout << "mob type:" << (*it)->type() << " goes " << move << std::endl;
}
}
// DON'T FORGET TO RELEASE THE HEAP!
for ( auto it = mobs.begin(); it != mobs.end(); it++ )
{
delete (*it);
}
}
為什麼 vector 要用 Moveable* 而非 Moveable 呢? 記得 pass-by-reference 嗎? 如果寫成
std::vector mobs;
Monster mob_1, mob_2, mob_3;
mobs.push_back( mob_1 );
mobs.push_back( mob_2 );
mobs.push_back( mob_3 );
它會試著複製 mob_1,mob_2,mob_3 到 vector 裡面,可是這不像複製個整數 5 到 function stack 的 data 空間一樣單純,我們沒有告訴 compiler 怎麼複製 Monster 這個物件,也就是沒為 Monster 寫過 operator = 或同等的複製方法。因為不知道怎麼正確複製 Monster 到 function stack 的 data,compiler 常常會出現預期以外的行為,是種危險的寫法。
Static Function and Static Variable
思考一下 monster 和 player 的戰鬥,戰鬥的邏輯不該單獨存在於 Monster/Player 裡面,因為那是二個物件之間對等且雙方都會有影響的互動。在 C++ Programming Style 裏,所有的 function 都該寫在某個物件裏,我這邊的選擇是寫成 World 裡面的某個 static function。
參考下面的程式
#include <iostream>
#include "mob.hpp"
class World
{
public:
static const int world_version_ = 10422;
public:
static void attack( Moveable* attacker, Moveable* attackee)
{
// ATTACK!!!
}
};
int main()
{
Monster* monster = new Monster();
Player* player = new Player();
std::cout << "Version: " << World::world_version_ << std::endl;
World::attack( monster, player );
delete monster; // DON'T FORGET TO RELEASE RESOURCE!
delete player;
return 0;
}
static function 和 static variable 不需要建立實體的物件就能直接使用,是用時也不是用 . 或是 ->,而是 ::。對 static variable 來說,其實就跟 C 風格的 global variable 是一樣的意思,是個全域且共用的變數。
My First Design Pattern – “Singleton”
類似我們在上一小節思考在哪裡放 fight function,在寫大程式時,會出現很多可以用同時用多種方法解決的問題,但哪一種是對的? 哪一種未來要增減功能時痛苦指數最低? 哪一種最符合人腦的直覺想法? 哪一種最後不會讓你想把自己的名字從作者欄中刪掉? 這些程式設計上的選擇就是 Design Pattern。
現在,我們來使用一個常見的 Design Pattern, Singleton, 來處理 World 這個物件。
Singleton 指的是有一個物件,在全程式中是單一,共用,且一直存在不會消失的,也就是 C 的 global variable 的概念。對 RPG 來說,世界物件,包括了地圖或是遊戲參數,這些都該是單一且共用的東西。遊戲任何時候都不該建立二個相同的 “世界” 物件,”世界” 物件需要一直存在,除非程式結束 “世界” 永遠不該被消滅。這些特性都是 Singleton 的使用情境。
架構如下:
class World
{
public:
static World& get_instance()
{
static World instance;
return instance;
}
public:
World(const World& arg) = delete; // copy constructor
World& operator=(const World& arg) = delete; // assignment operator
private:
World() {}
public:
void tick()
{
}
};
int main()
{
World::get_instance().tick();
return 0;
}
- 永遠都只有一個的物件,絕對不能有 = 號的出現,因為 = 就代表了複製,但 compiler 會自動產生一個,所以要告知刪除以防萬一。
- 永遠都只有一個的物件,絕對不能讓外界使用它的 constructor 來複製,但 compiler 會自動產生一個,所以要告知刪除以防萬一。
- 預設的 Constructor 依然是必要的,放在 Private 讓使用者不能用。
- 唯一的實體存在於 class 宣告內的 static World instance,取用時是取得它的 reference (也就是可以直接使用,且不會被更改的 address)。
- 使用裡面的 function tick() 的方法如程式所述。
RPG!
作業: 下面是個離職員工寫的糟糕程式,是個半成品沒辦法 compile,想辦法完成它吧。看你要打掉重寫或是修改都行!
class Moveable
{
public:
Moveable( int hp )
{
hp_ = hp;
attacking_ = NULL;
}
virtual int next_move();
// accessor
void x( int x ) { x_ = x; }
void y( int y ) { y_ = y; }
void hp( int hp ) { hp_ = hp; }
int x() { return x_; }
int y() { return y_; }
int hp() { return hp_; }
private:
int x_, y_, hp_;
Moveable *attacking_;
};
class Monster : public Moveable
{
public:
int next_move() override
{
return 1; // not-very-smart monster that can only go north
}
};
class Player : public Moveable
{
public:
int next_move() override
{
int move;
std::cout << "your next move? (1~4) "; std::cin >> move;
return move;
}
};
class World
{
public:
static World& get_instance()
{
static World instance;
return instance;
}
public:
World(const World& arg) = delete; // copy constructor
World& operator=(const World& arg) = delete; // assignment operator
public:
void tick()
{
if ( player_ is not fighting )
{
// show room description
std::cout << desc( player_.x(), player_.y() ) << std::endl;
// show exit, ex: "You can see two exit(s) [north, south]"
// show monster information, ex: "One monster is here."
}
for all monster in mobs_
{
if ( monster is in the same room with player_ )
{
if ( monster is not fighting )
{
set monster start attacking player_
show "monster start attacking you!"
if ( player_ is not fighting )
set player_ start attacking monster
}
}
if ( monster is not fighting )
{
int direction = monster->next_move();
if ( direction is valid )
{
monster->x( new x );
monster->y( new y );
if ( new x and new y is where the player is )
{
show "monster came from [n/e/w/s], combat is starting!"
set monster start attacking player
if player is not fighting
set player start attacking that monster
}
}
}
else // monster is fighting
{
attack( monster, player ); // shows battle information in this function
}
}
// player's move
if ( player is not fighting )
{
int direction = player->next_move();
if ( direction is valid )
{
player->x( new x );
player->y( new y );
}
}
else // player is fighting
{
attack( monster, player ); // shows battle information in this function
}
if ( player died )
{
// Good Game 🙂
}
// traversal all mobs
for all monster in mobs_
{
if ( monster dead )
{
1. remove from attacking target for player
2. change player's target to other mob if anyone is attacking him/her
}
}
}
private:
World()
{
// create map, this '{}' is c++11 syntax
map_ = new int[5*5]
{
1, 1, 1, 1, 1,
1, 0, 0, 0, 0,
1, 0, 0, 1, 0,
1, 1, 0, 1, 0,
1, 0, 0, 0, 0
};
// summon mobs
// create player
}
~World()
{
// delete player
// delete mobs
// delete map
delete [] map_;
}
bool create_monster( int hp, int x, int y )
{
return false;
}
bool create_player( int hp, int x, int y )
{
return false;
}
std::string desc( int x, int y )
{
std::ostringstream stringStream;
stringStream << "This is a empty room with a white board floating on the air. On the board, it wrote ("
<< x << "," << y << ")";
return stringStream.str();
}
void attack( Moveable* attacker, Moveable* attackee )
{
}
private:
int* map_;
std::vector<Monster*> mobs_;
Player* player_;
};
int main()
{
while (true)
{
World::get_instance().tick();
}
return 0;
}
*** 遊戲開始 ***
你走在一個峽谷裏,左右是高山,只有前後有路。
你看到可以走的出口是 [north(1), south(3)]
[HP 10] >> 1
你往北走
你走在一個峽谷裏,前方沒有路,只能往後走。
你看到可以走的出口是 [south(3)]
你看到前面有一隻怪物
你看到前面有一隻怪物
[HP 10] >>
怪物開始攻擊你
怪物開始攻擊你
怪物傷害你 1 HP [HP 9]
怪物傷害你 1 HP [HP 8]
你攻擊怪物 1 HP
南方來了一隻怪物
怪物開始攻擊你