目錄
- 1-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 使用EOS Studio
- 2-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 智能合約設計與實現(xiàn)
- 3-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 游戲公平性及安全性
- 4-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 加入Token體系
- 5-從零開發(fā)EOS區(qū)塊鏈小游戲系列 - 實現(xiàn)玩家免CPU玩游戲(終)
?Token并非區(qū)塊鏈獨有,在區(qū)塊鏈世界體現(xiàn)為一種權益證明。而且在不同的應用場景也叫法也可以不同,可以是票證、股份、代幣。而在EOS中,代幣符號就叫做EOS,代幣可以交易,可以購買內存、和計算網(wǎng)絡資源,如果次有代幣還可以擁有投票權。
?本章我們?yōu)樽约旱男∮螒蚪⒆约旱腡oken,下面我們稱為代幣,而代幣的符號(symbol)就叫SJ。上面說了EOS(后面紅字EOS統(tǒng)一指代幣)就是代幣,從技術角度來看,代幣沒有什么特殊的,他就是在一個智能合約里面一張表的記錄而已,所以要有代幣必先有合約賬號,EOS也不例外,他的合約賬號是:eosio.token;如果你擁有EOS,那么在eosio.token的accounts這張表就會有你的記錄,balance字段記錄了你擁有多少EOS??梢渣c擊https://eospark.com/contract/eosio.token 看看這個合約的信息??赡苣銜枺喝绻蠹业拇鷰欧柖冀?code>EOS怎么辦?代幣的符號是可以重復的,是合法的。那出現(xiàn)多個豈不是就區(qū)分不了?所以在校驗的時候必須加上合約賬號一起校驗,去年很多項目被攻擊就是因為只校驗了EOS,沒有校驗合約賬號。
?如果你只是想單純的發(fā)布一種屬于自己的代幣,那么其實你一句代碼都不需要寫,EOS對應的智能合約源代碼,官方是開源了的,拿到智能合約,編譯一下,生成wasm和abi文件,就可以直接部署了。然后調用簽名部署好的合約,通過create(創(chuàng)建token)的ACTION,就可以了。整個過程一分鐘之內就可以完成。
?但我們這里需要聯(lián)動,結合到我們的小游戲來提現(xiàn)代幣的價值,所以還是需要做一些增量改造,首先看下EOS的智能合約源代碼:https://app.eosstudio.io/eosio/eosio.token,注意這份合約代碼是1.6版本的,我們整個小游戲都是基于v1.3x+版本。源碼有幾個重要的ACTION:
...
/**
* 創(chuàng)建代幣
* @param isuser 發(fā)行人賬號,當敏感操作需要使用該賬號權限
* @param maximum_supply 代幣最大發(fā)行量,EOS是10000000000
*/
[[eosio::action]]
void create( const name& issuer,
const asset& maximum_supply);
/**
* 發(fā)行代幣,一般“挖礦”操作就是使用此接口,前提必先執(zhí)行create
* @param to 代幣接收人賬號
* @param quantity 發(fā)行的代幣金額
* @param memo 備注
*/
[[eosio::action]]
void issue( const name& to, const asset& quantity, const string& memo );
...
/** 轉賬,前提必先執(zhí)行issue
* @param from 轉賬發(fā)起人賬號
* @param to 代幣接收人賬號
* @param quantity 轉賬的代幣金額
* @param memo 備注
*/
[[eosio::action]]
void transfer( const name& from,
const name& to,
const asset& quantity,
const string& memo );
?上面的幾個對外接口都有說明了,現(xiàn)在我們新增一個miner代碼如下:
void token::miner( const name& to, const asset& quantity, const string& memo ){
//1. 必須由小游戲合約權限調用
require_auth("kingofighter"_n);
//2. 先發(fā)行代幣給合約自身
auto sym = quantity.symbol;
stats statstable( _self, sym.code().raw() );
auto existing = statstable.find( sym.code().raw() );
const auto& st = *existing;
statstable.modify(st, same_payer, [&](auto &s) {
s.supply += quantity;
});
//3. 再由合約轉賬給接收人‘to’
if (to != st.issuer) {
add_balance(st.issuer, quantity, st.issuer);
SEND_INLINE_ACTION(*this, transfer, { st.issuer, "active"_n }, { st.issuer, to, quantity, memo });
}
}
?注意此action只能被小游戲的合約賬號來調用,調用流程會在下面給出;上面代碼其實分了兩個步驟:
- 首先token合約發(fā)行代幣,代幣的持有人這時候是token合約自身(注釋2部分)
- 然后token合約再將自己的代幣轉賬給入?yún)⒌?code>to接收者,
SEND_INLINE_ACTION是一個行內操作,是事務性的,所以整個action操作要么成功要么失敗。
?token合約的代碼編寫好之后,先為代幣創(chuàng)建一個EOS賬號:

?然后創(chuàng)建一個項目(不清楚可以調到第一、二章),直接把上面eosio.tokenhpp和cpp文件的代碼復制復制到你的合約里面,這時候目錄應該是這樣:

?然后打開你的.hpp文件,把以下這一行修改一下:
class [[eosio::contract("改為你的合約賬號")]] token : public contract
?接著打開.cpp文件,
#include <eosio.token.hpp>
改為
#include <你的合約賬號.hpp>
//其實就是上面目錄結構圖include文件里面的hpp文件名
?最后就是編譯-》部署到麒麟測試鏈,你還需要為合約購買內存和CPU,這些在第一章都有說,這里就不重復了。
?現(xiàn)在我們已經(jīng)將合約代碼部署到區(qū)塊鏈了,接著就是創(chuàng)建我們的代幣,打開EOS studio,切換到代幣的合約界面,選擇create action:

?入?yún)ⅲ?p>
- issuser:指定代幣發(fā)行人賬號,為什么這里指定游戲的合約賬號呢?我們下面會說。
- maximum_supply:最大發(fā)行量為100萬,
EOS發(fā)行量是100億,表示我們的代幣還是比較稀有的。
?第三個參入是調用權限,需要填寫本合約的賬號,因為create校驗了必須本合約權限調用,這是代碼確定的:
void token::create( const name& issuer,
const asset& maximum_supply )
{
require_auth( _self ); //必須本合約賬號權限
...
}
?完成后,可以在界面右邊看到數(shù)據(jù),SUPPLY值為0,表示目前還沒有發(fā)行任何代幣:

?以上,我們的代幣已經(jīng)就緒,但是什么時候應該發(fā)行代幣?不發(fā)行就沒有交易,沒有流通也就沒有價值?;氐轿覀兊男∮螒?,還記得游戲邏輯是如果玩家勝利了,會獎勵一定的SJ,這里其實就是一個發(fā)行的過程,也可以叫做挖礦。所以我們需要修改一下小游戲合約的代碼,讓他和我們的代幣結合起來使用。

?留意上圖,對比上一章的流程,新增了兩個功能,一個是第三步的可支持氪金以及發(fā)行代幣;氪金邏輯:我們決定10個SJ(代幣的符號,在游戲里代表水晶。無特殊說明下面SJ均表示代幣)可以提高1點攻擊力,這樣就可以提高玩家的勝率。那這里就涉及到轉賬需求了,如果想氪金,玩家就要從自己的賬號轉賬給小游戲的賬號,回想上一章,開始一局游戲時,玩家調用了合約的newgameaction,最開始的想法是可不可以調用的同時,附帶轉賬功能呢?ETH就是這樣設計的,但EOS這里不行。
?至于EOS為什么不行?我個人的理解是:ETH和EOS的設計不同,ETH轉賬是一個特殊的操作,和普通的調用操作不一樣;但EOS本質上轉賬其實也是action,一個在合約賬號eosio.token上名稱為transfer的action,和普通的action并無區(qū)別,所以你想想如果你想調用newgame同時轉賬,實際就是想同時調用兩個action了,所以不被允許。
?但反過來:轉賬同時附加執(zhí)行邏輯卻是可以的,我們利用一種比較巧妙的方法,不過實現(xiàn)起來似乎有點別扭,在這之前我們講講轉賬:EOS所有的賬號包含智能合約賬號都可以接受轉賬,標準的轉賬action有4個參數(shù):發(fā)起人、接收人、轉賬金額、備注。所以如果“接收人”填的是一個智能合約的賬號,表示給這個合約轉賬。還有一點,EOS在執(zhí)行轉賬的時候,會通知到“接收人”,“接收人”可以在自己的合約代碼捕獲這個通知。
?到這里其實比較清楚了:智能合約可以通過轉賬通知,知道有人給我轉賬了,再根據(jù)轉賬時填寫的“備注”,就可以知道需要做哪些操作,比如我們可以在轉賬備注填寫:action:newgame,param1:xx,param2:xx...,這樣合約就可以知道需要執(zhí)行哪些操作,入?yún)⑹鞘裁?。是不是很巧妙呢:?br>
?切換回到小游戲的合約代碼.hpp文件,有兩處需要新增代碼,一處是新增一個交易的action,另一處在代碼的最最底部新增一段:
...
//注意,這里交易的action沒有聲明為[[eosio::action]]
//所以不會出現(xiàn)在abi文件中,即表示這是非公開的
void transfer(const name from,const name to,const asset &quantity, const string memo);
...
extern "C"
{
//由于EOS有類似通知的功能,執(zhí)行某些操作時,你可以指定通知給其他賬號
//這里能接收所有的消息,入?yún)? //`receiver`表示接收通知的賬號
//`code`表示發(fā)出通知的合約賬號
//`actin`表示發(fā)出通知的合約賬號所被調用的action
void apply(uint64_t receiver, uint64_t code, uint64_t action) {
//校驗,必須是`kofgametoken`合約的交易操作,才能執(zhí)行以下的邏輯
if (code == name("kofgametoken").value && action == name("transfer").value) {
//把接收到的參數(shù)透傳到我們自己定義的`transfer` action
//等價于捕捉到轉賬后,執(zhí)行我們自己的`tranfer`
//就是我們上面定義的tranfer
execute_action(name(receiver), name(code), &kingofighter::transfer);
return;
}
if (code != receiver)
return;
switch (action) {
EOSIO_DISPATCH_HELPER(kingofighter, (signup)(battle)(newgame))
}
eosio_exit(0);
}
}
?接下來就是編寫捕捉到轉賬后,需要執(zhí)行的邏輯,我們把這塊代碼放在一個transfer的action,其實就是把上一章的newgame里面的代碼,只是需要作一點修改:
ACTION kingofighter::transfer(const name from,const name to, const asset &quantity,const string memo) {
//這一句很重要,涉及到安全問題
//from == get_self() 的時候 return;表示當轉賬發(fā)起人是合約自身時,跳過
//to != get_self() 的時候return;表示接受這并不是合約自身時,跳過
//什么情況會出現(xiàn)to != get_self()?當其他合約發(fā)起通知的時候即:require_recipient操作,有興趣可以查一下,這里不展開
if (from == get_self() || to != get_self()) return;
//2. 普通轉賬,無需執(zhí)行邏輯
if (memo.empty()) return;
//3. 只接受SJ的代幣
const symbol SJ = symbol(symbol_code("SJ"), 4);
check(quantity.symbol == SJ, "only SJ token allowed");
check(quantity.is_valid(), "quantity invalid");
check(quantity.amount >=10*1000,"quantity at least 10 SJ");
//4. 解析備注
// 入?yún)⒁还?個:action、user_seed、house_seed_hash、expire_timestamp、sig
vector<string> vec;
split_memo(vec, memo, ',');
if(vec.size() != 5)
return;
//5. 入?yún)㈩愋娃D換
const string action = split_val(vec,"action");
check("imrich"==action,"action invalid");
string user_seed = split_val(vec,"us");
string house_seed_hash_str = split_val(vec,"ush");
checksum256 house_seed_hash = hex_to_sha256(house_seed_hash_str);
uint64_t expire_timestamp = stoll(split_val(vec,"et"));
signature sig = str_to_sig(split_val(vec,"sig"));
...
g_tb.emplace(get_self(), [&](auto &r) {
...
r.coin = quantity; //記錄玩家氪金的金額
...
});
?這里需要強調,上面return,并不是拒絕調用者的請求;而是不執(zhí)行下面的邏輯而已,轉賬依然是能成功的,但如果check檢查不通過,拋出了異常,轉賬也會失敗。代碼邏輯都寫了注釋這里不多說。
?然后是修改battle,需要新增挖礦的邏輯:
ACTION kingofighter::battle(const uint64_t& game_id,const string &house_seed) {
require_auth(get_self());
...
if (i & 1) {
//i為奇數(shù),BOSS攻擊
...
} else {
//i為偶數(shù),玩家攻擊
uint32_t hero_max_atk = hero->max_atk;
uint32_t hero_min_atk = hero->min_atk;
if(itr->coin.amount > 0){
//玩家已氪金 10SJ=1攻擊力
//最高只能增加20點攻擊力
uint32_t append_atk = itr->coin.amount /1000 / 10;
if(append_atk > 20)
append_atk = 20;
hero_max_atk += append_atk;
hero_min_atk += append_atk;
}
damage = hash_val % (hero_max_atk - hero_min_atk + 1) + hero_min_atk;
...
if (hero_hp > 0) {
...
//并轉賬代幣給玩家
const asset reward_coin = asset(100 * 10000, symbol(symbol_code("SJ"), 4));
action(permission_level{get_self(), "active"_n},
"kofgametoken"_n, "miner"_n,
std::make_tuple(player,reward_coin,"Reward SJ.")).send();
}
?主要修改了兩處地方:一處是當玩家進行攻擊,且玩家已氪金,攻擊力需要增加,但最高只能增加20點;另外一處是當玩家獲勝,需要轉賬代幣給玩家(挖礦)。
if (hero_hp > 0) {...}這塊代碼是使用本合約賬號的權限,調用kofgametoken合約的miner action。
?現(xiàn)在,代碼都已經(jīng)編寫完畢,但在開始對戰(zhàn)之前,如果想要氪金,玩家還需要有SJ,但目前玩家并沒有任何SJ代幣,我們可以贈送一些給他用于內部測試,打開EOS studio,切換到Token智能合約界面,選擇miner,決定贈送500個SJ給玩家:

?上面注意需要使用小游戲的智能合約的賬號權限來調用,執(zhí)行成功后,可以看到右邊
stat表的發(fā)行量數(shù)據(jù)變?yōu)?code>500了。我們可以查看玩家這時候的 SJ余額:
?一切準備就緒,我們來看看執(zhí)行的效果。先啟動服務端,不清楚的朋友請回到第三章。獲取種子信息:
{
"house_seed_hash":"6e64e182e42920b689368ebc732188dbe7ac7c63939495f2671ad7d64937b0a9",
"expire_timestamp":1579072709,
"sig":"SIG_K1_Jx3ahMXUUxyEm2wYSXaJoXTsXhMGTC4g76dPxrEm58AF6gLEh3GVGSvWn4hDTq1bsKLeY1RfRAhfkzYz1wM6REChrWVLZi"
}
?打開EOS studio,依然是切換到Token合約,選擇transfer action:

- 輸入玩家的賬號
- 輸入小游戲的智能合約賬號
- 輸入氪金金額(注意格式)
- 備注,需要把入?yún)⑵唇?/li>
根據(jù)我們獲取到的種子信息,我們這里拼接到的數(shù)據(jù)是:
action:imrich,us:9a114079014a,ush:6e64e182e42920b689368ebc732188dbe7ac7c63939495f2671ad7d64937b0a9,et:1579072709,sig:SIG_K1_Jx3ahMXUUxyEm2wYSXaJoXTsXhMGTC4g76dPxrEm58AF6gLEh3GVGSvWn4hDTq1bsKLeY1RfRAhfkzYz1wM6REChrWVLZi
點擊執(zhí)行后,等一會服務端自動調用對戰(zhàn)后,再看看gamerecords表的對戰(zhàn)結果,顯示勝利。仔細查看每一次對BOSS造成的傷害明顯增加了不少:

?如果這時候你查看玩家的余額會發(fā)現(xiàn)還有
400=500-200+100,表示挖礦成功?;蛘吣阋部梢酝ㄟ^日志查看整個執(zhí)行過程,不過之前的日志是依賴history_plugin插件,好像是EOS1.3x后已經(jīng)不建議使用,現(xiàn)在基本所有超級節(jié)點也都停用了,好在有一些平臺提供了日志的服務,我自己使用的就是dfuse平臺提供的接口。?到這里,我們的小游戲全部邏輯已經(jīng)搭建完成。一般會提供UI界面去給玩家操作,然后將攻擊特效做得很炫酷,這時候玩家調用合約就沒有那么方便,因為涉及到私鑰簽名,需要借助官方的
eosjsjs庫,以及依賴scatter。?下一章是本系列的最終章,將會講解如何實現(xiàn)EOS1.8版本的一個新型功能:
ONLY_BILL_FIRST_AUTHORIZER,即如何讓玩家不需要支付CPU和NET,就可以玩我們的小游戲。敬請期待:)
本章節(jié)源代碼地址:https://github.com/jan-gogogo/kof-chapter4