目錄
- 1-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 使用EOS Studio
- 2-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 智能合約設(shè)計(jì)與實(shí)現(xiàn)
- 3-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 游戲公平性及安全性
- 4-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 加入Token體系
- 5-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 實(shí)現(xiàn)玩家免CPU玩游戲(終)
智能合約表結(jié)構(gòu)設(shè)計(jì)
?回顧上一章的游戲規(guī)則,首先必須有一張玩家表,用來保存注冊玩家的數(shù)據(jù),還要有英雄表,用來存放出戰(zhàn)英雄的數(shù)據(jù)。最后一張寶箱表,存放玩家所獲得的寶箱,結(jié)構(gòu)如下:
玩家表:
- 玩家EOS賬號
- 擁有的SJ token(水晶)
- 游戲局?jǐn)?shù)計(jì)數(shù)器
英雄表:
- 唯一id
- 英雄名稱
- 攻擊力最小值
- 攻擊力最大值
- 血量
寶箱表:
- 唯一id
- 所屬玩家
- 寶箱的級別(金、銀、銅)
//player
//scope is self
TABLE players {
name player_account;
asset coin;
uint64_t created_at;
uint64_t primary_key() const { return player_account.value;}
};
//hero
//scope is player
TABLE heros {
uint64_t id = 10000;
string hero_name;
uint32_t atk;
uint32_t hp;
uint64_t created_at;
uint64_t primary_key() const { return id;}
};
//box
//scope is self
//index: player_account
TABLE boxs {
uint64_t id = 10000;
name player_account;
uint8_t level;
uint64_t created_at;
uint64_t get_player() const { return player_account.value;}
uint64_t primary_key() const { return id;}
};
using player_index = multi_index<"players"_n, players>;
using hero_index = multi_index<"heros"_n, heros>;
using box_index = multi_index<"boxs"_n, boxs,
indexed_by<"byplayer"_n, const_mem_fun<boxs, uint64_t, &boxs::primary_key>>
>;
?uint64_t primary_key() const { return player_account.value;}表示將player_account 設(shè)置為表的主鍵,主鍵具有唯一約束,且每個(gè)表都有一個(gè)主鍵。
?第三個(gè)boxs表的uint64_t get_player() const { return player_account.value;}表示將player_account設(shè)為索引,是為了后面方便查詢表的數(shù)據(jù)。
??最后using開頭的三行是對表進(jìn)行配置:multi_index表明示多索引表,即可以存放多條數(shù)據(jù),另外一種singleton聲明的,表示只能存放一條數(shù)據(jù),但相對地CRUD操作就非常方便。一搬用在存放合約全局配置數(shù)據(jù)的表。
??大家看上面heros和boxs表的結(jié)構(gòu)有沒有發(fā)現(xiàn)有些問題,boxs有一個(gè)所屬玩家的字段,而heros卻沒有。那怎么查詢某個(gè)玩家下的hero呢?答案是根據(jù)表的scope來查詢的,什么是scope?我們先看下使用一個(gè)表之前,對表進(jìn)行實(shí)例化的代碼:
box_index box_table(get_self(), gete_self().value ); //實(shí)例化boxs表
hero_index hero_table(get_self(),"bob"_n.value ); //實(shí)例化heros表
實(shí)例化需要兩個(gè)參數(shù):
- 第一個(gè)參數(shù)指定表的擁有者(owner),owner的賬號需要為存入該表的數(shù)據(jù)支付RAM,表的數(shù)據(jù)也只能owner能夠修改。我們使用get_self()表示使用合約的賬號。
- 第二個(gè)參數(shù)用來將數(shù)據(jù)分區(qū),如上代碼,如果傳入玩家bob的賬號,那么我們拿到的hero_table就是一個(gè)只針對bob的數(shù)據(jù),也只能對部分?jǐn)?shù)據(jù)進(jìn)行操作。不知道大家發(fā)現(xiàn)沒有,如果你要查詢這張表的所有數(shù)據(jù),是沒有辦法查的,因?yàn)閟cope必須指定。eosio.token 的accounts也是用戶賬戶作為scope。
?使用scope區(qū)分和使用索引其實(shí)都可以達(dá)到目的,至于使用的時(shí)機(jī)就要看情況。還是以上面兩個(gè)表為例,根據(jù)我之前的經(jīng)驗(yàn),個(gè)人認(rèn)為如果在同一個(gè)交易中,如果需要同時(shí)操作多個(gè)玩家的英雄數(shù)據(jù),就最好使用索引了,例如:在某個(gè)版本,需要給所有攻擊力低于30的英雄+10攻擊力,因?yàn)橛X得英雄太弱雞了。這時(shí)查詢的維度不是玩家,而是攻擊力,如果將攻擊力設(shè)置為索引,就很方便了。這里heros用scope的方式,是因?yàn)榇_保每次交易,我們只操作當(dāng)前玩家的數(shù)據(jù)。
智能合約代碼編寫
?還記得上一章節(jié)新建的項(xiàng)目,有兩個(gè)文件.hpp和.cpp,hpp用來編寫對外描述action、表結(jié)構(gòu)和一些私有方法和變量。cpp主要編寫action的具體實(shí)現(xiàn)。
?按照上一章游戲規(guī)則的順序,先編寫注冊的action,打開hpp文件,把系統(tǒng)自動生成的 ACTION hi(name user)刪掉,替換為:ACTION signup(const name player);。在實(shí)現(xiàn)前回顧下游戲規(guī)則:
玩家需要注冊賬號,同時(shí)獲得1000個(gè)SJ幣(水晶),并得到一個(gè)人物用于戰(zhàn)斗,人物有攻擊力和血量2個(gè)屬性,攻擊力初始35-70,血量初始500。
?切換到.cpp文件,實(shí)現(xiàn)如下:
ACTION kingofighter::signup(name player) {
//要求必須玩家本人注冊
require_auth(player);
//實(shí)例化player表
player_index player_tb(get_self(),get_self().value);
//主鍵獲取玩家的數(shù)據(jù)
auto itr = player_tb.find(player.value);
//如果玩家數(shù)據(jù)已存在,拋出異常
check(itr==player_tb.end(), "player account exist!" );
//聲明水晶數(shù)量1000個(gè) 乘10000是為了抵消0.0001
const uint64_t amt = 1000 * 10000;
//插入一條玩家數(shù)據(jù)
player_tb.emplace(get_self(), [&]( auto& r ) {
r.player_account = player; //玩家賬號
r.coin = asset(amt, symbol(symbol_code("SJ"), 4);); //初始水晶數(shù)量:1000
r.counter = 0; //玩家游戲局?jǐn)?shù)
r.created_at = time_point_sec(current_time_point()); //當(dāng)前區(qū)塊鏈時(shí)間
});
//實(shí)例化hero表
//第二個(gè)入?yún)?scope)為玩家賬號
hero_index hero_tb(get_self(),player.value);
hero_tb.emplace(get_self(), [&]( auto& r ) {
r.id = hero_tb.available_primary_key();
r.hero_name = "jakiro"; //英雄名稱:杰奇諾
r.min_atk = 35; //攻擊力最小值
r.max_atk = 70; //攻擊力最大值
r.hp = 500; //血量
r.created_at = time_point_sec(current_time_point()); //當(dāng)前區(qū)塊鏈時(shí)間
});
}
?入?yún)樽酝婕业腅OS賬號,注冊action可分三部分:校驗(yàn)權(quán)限、插入玩家數(shù)據(jù)、插入英雄數(shù)據(jù)。上面代碼根據(jù)注釋很好理解,但又兩句需要說下r.coin = asset(1000 * 10000, symbol(symbol_code("SJ"), 4));,這里構(gòu)造1000個(gè)符號為“SJ”,小數(shù)點(diǎn)后保留4位的asset資產(chǎn)。r.id = hero_tb.available_primary_key();表示使用hero表維護(hù)的自增id,默認(rèn)從0開始自增。
?注冊有了,現(xiàn)在可以開始構(gòu)思對戰(zhàn)的過程。對于玩家來說,應(yīng)該是只需要調(diào)用一個(gè)[對戰(zhàn)]的action,然后等待戰(zhàn)斗結(jié)果就可以了。再仔細(xì)想想會發(fā)覺,其實(shí)目前所有戰(zhàn)局的因素都是固定的:玩家英雄的屬性、BOSS的屬性。所以需要外部給一個(gè)生產(chǎn)隨機(jī)的因素。為了提高玩家的參與感,讓他覺得可以影響到戰(zhàn)局,我們允許玩家提供一個(gè)隨機(jī)數(shù),然后再結(jié)合我們在合約的時(shí)間戳,產(chǎn)生一個(gè)新的隨機(jī)數(shù)。對戰(zhàn)邏輯中的所有動作都將會與這個(gè)隨機(jī)數(shù)相關(guān),下面貼出部分核心代碼:
ACTION kingofighter::battle(const name player, const capi_checksum256 &seed_hash) {
...
const uint32_t NOW_TS = current_time_point().sec_since_epoch();
const uint32_t BOSS_MIN_ATK = 50;
const uint32_t BOSS_MAX_ATK = 70;
const uint32_t BOSS_HP = 700;
uint32_t hero_hp = hero->hp;
uint32_t boss_hp = BOSS_HP;
...
for (size_t i = 0; i < 32; i++) {
const uint32_t hash_val =(uint32_t) seed_hash.extract_as_byte_array()[i] + NOW_TS;
uint32_t damage;
if (i & 1) {
//i為奇數(shù),BOSS攻擊
damage = hash_val % (BOSS_MAX_ATK - BOSS_MIN_ATK + 1) + BOSS_MIN_ATK;
hero_hp = hero_hp > damage ? hero_hp - damage : 0;
} else {
//i為偶數(shù),玩家攻擊
damage = hash_val % (hero->max_atk - hero->min_atk + 1) + hero->min_atk;
//是否暴擊,暴擊概率25%
if (hash_val % 4 == 0)
damage += 100;
boss_hp = boss_hp > damage ? boss_hp - damage : 0;
}
//這一輪的戰(zhàn)斗結(jié)果
scoreboard sb_item = {
.round_no = i + 1,
.attacker = i & 1 ? get_self() : player,
.defender = i & 1 ? player : get_self(),
.damage = damage,
.defender_hp = i & 1 ? hero_hp : boss_hp
};
scoreboards.emplace_back(sb_item);
//如果任何一方血量歸0,戰(zhàn)斗結(jié)束
if (hero_hp == 0 || boss_hp == 0)
break;
}
...
}
?首先看看第二個(gè)入?yún)?code>seed_hash 是一個(gè)checksum256類型,其實(shí)就是一個(gè)長度64的哈希值。也就是玩家的隨機(jī)數(shù)算哈希:hash(random)。我們在使用的時(shí)候體現(xiàn)為一個(gè)uint8_t[32],分布為32個(gè)0-255的數(shù)字。這個(gè)32個(gè)數(shù)字加上當(dāng)前時(shí)間戳就是隨機(jī)數(shù),且BOSS最小攻擊50,玩家英雄血量500,假設(shè)每次攻擊都臉黑,BOSS擊敗玩家英雄需要500 / 50 = 10 輪,10 * 2 < 32,所以我們目前的需求,32個(gè)夠用了。我們來運(yùn)行一下看結(jié)果:
- 切回到EOS Studio,構(gòu)建,然后部署。
- 創(chuàng)建一個(gè)玩家賬號:點(diǎn)擊右上角-》Create Account,這里輸入 sweetsummer1
-
點(diǎn)擊右上角-》Contract,選擇你的合約賬號,可以看到一下界面:
區(qū)域1是合約的action列表,及每個(gè)action的入?yún)ⅲ瑓^(qū)域2是合約的表數(shù)據(jù)。
- 我們選擇
signup,在入?yún)⑻顚憇weetsummer1,即需要注冊的玩家賬號:
注冊后表數(shù)據(jù) - 注冊成功,開始調(diào)用對戰(zhàn)的action,需要兩個(gè)入?yún)ⅲ簆layer和seed_hash,其中player填寫剛注冊的賬號,seed_hash需要一個(gè)64長度的字符,可以在http://tool.oschina.net/encrypt?type=2網(wǎng)站生成一個(gè),隨便輸入一些東西, 點(diǎn)擊生成 sha256:
對賬結(jié)果
看到對戰(zhàn)的一局結(jié)果已經(jīng)出來,且是玩家獲勝,右邊的數(shù)據(jù)表示每一輪攻擊的詳細(xì)信息,有發(fā)動攻擊者、攻擊血量、是否暴擊等... - 再將表切換到
boxs,可以看到玩家獲得了一個(gè)銅寶箱:
?就這樣,按理說對戰(zhàn)的結(jié)果受玩家的影響,也受時(shí)間戳的影響。好像一切都很公平很順利,但其實(shí)是這種使用時(shí)間戳生成隨機(jī)數(shù)的方法是可以被攻擊的,攻擊過程大概是攻擊者事先計(jì)算好未來的哪個(gè)時(shí)間戳對自己有利,然后當(dāng)?shù)竭_(dá)這個(gè)時(shí)間點(diǎn),才去調(diào)用對戰(zhàn)action。例如:攻擊者通過計(jì)算,知道在1569643270這個(gè)時(shí)間戳發(fā)動攻擊,是一定會獲勝,所以他自己寫一個(gè)智能合約,當(dāng)?shù)竭_(dá)這個(gè)時(shí)間點(diǎn),發(fā)動攻擊就可以了,十分簡單,為什么一定要使用智能合約來調(diào)用呢?因?yàn)檫@樣可以保證時(shí)間一致。
?怎么樣防止攻擊?現(xiàn)在開發(fā)者應(yīng)該都已經(jīng)達(dá)成共識,純鏈上生成隨機(jī)數(shù)是不可靠的,官方也不建議,我們需要合約開發(fā)者也生成一個(gè)隨機(jī)哈希,然后加上玩家的隨機(jī)哈希,合并在一起,產(chǎn)生隨機(jī)數(shù)。但這種做法有個(gè)缺點(diǎn),就是需要有個(gè)server端,下一章節(jié)我們會server端的設(shè)計(jì),以及智能合約的相關(guān)改動。
本章節(jié)源代碼地址:https://github.com/jan-gogogo/kof-chapter2



