目录
- 1-从零开发EOS区块链小游戏系列 - 使用EOS Studio
- 2-从零开发EOS区块链小游戏系列 - 智能合约设计与实现
- 3-从零开发EOS区块链小游戏系列 - 游戏公平性及安全性
- 4-从零开发EOS区块链小游戏系列 - 加入Token体系
- 5-从零开发EOS区块链小游戏系列 - 实现玩家免CPU玩游戏(终)
Token并非区块链独有,在区块链世界体现为一种权益证明。而且在不同的应用场景也叫法也可以不同,可以是票证、股份、代币。而在EOS中,代币符号就叫做EOS
,代币可以交易,可以购买内存、和计算网络资源,如果次有代币还可以拥有投票权。
本章我们为自己的小游戏建立自己的Token,下面我们称为代币,而代币的符号(symbol)就叫SJ
。上面说了EOS
(后面红字EOS统一指代币)就是代币,从技术角度来看,代币没有什么特殊的,他就是在一个智能合约里面一张表的记录而已,所以要有代币必先有合约账号,EOS
也不例外,他的合约账号是:eosio.token
;如果你拥有EOS
,那么在eosio.token
的accounts
这张表就会有你的记录,balance
字段记录了你拥有多少EOS
。可以点击https://eospark.com/contract/eosio.token 看看这个合约的信息。可能你会问:如果大家的代币符号都叫EOS
怎么办?代币的符号是可以重复的,是合法的。那出现多个岂不是就区分不了?所以在校验的时候必须加上合约账号一起校验,去年很多项目被攻击就是因为只校验了EOS
,没有校验合约账号。
如果你只是想单纯的发布一种属于自己的代币,那么其实你一句代码都不需要写,EOS
对应的智能合约源代码,官方是开源了的,拿到智能合约,编译一下,生成wasm和abi文件,就可以直接部署了。然后调用签名部署好的合约,通过create
(创建token)的ACTION,就可以了。整个过程一分钟之内就可以完成。
但我们这里需要联动,结合到我们的小游戏来提现代币的价值,所以还是需要做一些增量改造,首先看下EOS
的智能合约源代码:https://app.eosstudio.io/eosio/eosio.token,注意这份合约代码是1.6版本的,我们整个小游戏都是基于v1.3x+版本。源码有几个重要的ACTION:
...
/**
* 创建代币
* @param isuser 发行人账号,当敏感操作需要使用该账号权限
* @param maximum_supply 代币最大发行量,EOS是10000000000
*/
[[eosio::action]]
void create( const name& issuer,
const asset& maximum_supply);
/**
* 发行代币,一般“挖矿”操作就是使用此接口,前提必先执行create
* @param to 代币接收人账号
* @param quantity 发行的代币金额
* @param memo 备注
*/
[[eosio::action]]
void issue( const name& to, const asset& quantity, const string& memo );
...
/** 转账,前提必先执行issue
* @param from 转账发起人账号
* @param to 代币接收人账号
* @param quantity 转账的代币金额
* @param memo 备注
*/
[[eosio::action]]
void transfer( const name& from,
const name& to,
const asset& quantity,
const string& memo );
上面的几个对外接口都有说明了,现在我们新增一个miner
代码如下:
void token::miner( const name& to, const asset& quantity, const string& memo ){
//1. 必须由小游戏合约权限调用
require_auth("kingofighter"_n);
//2. 先发行代币给合约自身
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合约发行代币,代币的持有人这时候是token合约自身(注释2部分)
- 然后token合约再将自己的代币转账给入参的
to
接收者,SEND_INLINE_ACTION
是一个行内操作,是事务性的,所以整个action操作要么成功要么失败。
token合约的代码编写好之后,先为代币创建一个EOS账号:
使用EOS studio创建账号
然后创建一个项目(不清楚可以调到第一、二章),直接把上面eosio.token
hpp和cpp文件的代码复制复制到你的合约里面,这时候目录应该是这样:
然后打开你的.hpp文件,把以下这一行修改一下:
class [[eosio::contract("改为你的合约账号")]] token : public contract
接着打开.cpp文件,
#include <eosio.token.hpp>
改为
#include <你的合约账号.hpp>
//其实就是上面目录结构图include文件里面的hpp文件名
最后就是编译-》部署到麒麟测试链,你还需要为合约购买内存和CPU,这些在第一章都有说,这里就不重复了。
现在我们已经将合约代码部署到区块链了,接着就是创建我们的代币,打开EOS studio,切换到代币的合约界面,选择create
action:
入参:
- issuser:指定代币发行人账号,为什么这里指定游戏的合约账号呢?我们下面会说。
- maximum_supply:最大发行量为100万,
EOS
发行量是100亿,表示我们的代币还是比较稀有的。
第三个参入是调用权限,需要填写本合约的账号,因为create
校验了必须本合约权限调用,这是代码确定的:
void token::create( const name& issuer,
const asset& maximum_supply )
{
require_auth( _self ); //必须本合约账号权限
...
}
完成后,可以在界面右边看到数据,SUPPLY
值为0,表示目前还没有发行任何代币:
以上,我们的代币已经就绪,但是什么时候应该发行代币?不发行就没有交易,没有流通也就没有价值。回到我们的小游戏,还记得游戏逻辑是如果玩家胜利了,会奖励一定的SJ
,这里其实就是一个发行的过程,也可以叫做挖矿。所以我们需要修改一下小游戏合约的代码,让他和我们的代币结合起来使用。
留意上图,对比上一章的流程,新增了两个功能,一个是第三步的可支持氪金以及发行代币;氪金逻辑:我们决定10个SJ
(代币的符号,在游戏里代表水晶。无特殊说明下面SJ
均表示代币)可以提高1点攻击力,这样就可以提高玩家的胜率。那这里就涉及到转账需求了,如果想氪金,玩家就要从自己的账号转账给小游戏的账号,回想上一章,开始一局游戏时,玩家调用了合约的newgame
action,最开始的想法是可不可以调用的同时,附带转账功能呢?ETH
就是这样设计的,但EOS
这里不行。
至于EOS
为什么不行?我个人的理解是:ETH
和EOS
的设计不同,ETH转账是一个特殊的操作,和普通的调用操作不一样;但EOS
本质上转账其实也是action,一个在合约账号eosio.token
上名称为transfer
的action,和普通的action并无区别,所以你想想如果你想调用newgame
同时转账,实际就是想同时调用两个action了,所以不被允许。
但反过来:转账同时附加执行逻辑却是可以的,我们利用一种比较巧妙的方法,不过实现起来似乎有点别扭,在这之前我们讲讲转账:EOS
所有的账号包含智能合约账号都可以接受转账,标准的转账action有4个参数:发起人、接收人、转账金额、备注。所以如果“接收人”填的是一个智能合约的账号,表示给这个合约转账。还有一点,EOS
在执行转账的时候,会通知到“接收人”,“接收人”可以在自己的合约代码捕获这个通知。
到这里其实比较清楚了:智能合约可以通过转账通知,知道有人给我转账了,再根据转账时填写的“备注”,就可以知道需要做哪些操作,比如我们可以在转账备注填写:action:newgame,param1:xx,param2:xx...
,这样合约就可以知道需要执行哪些操作,入参是什么。是不是很巧妙呢:)
切换回到小游戏的合约代码.hpp文件,有两处需要新增代码,一处是新增一个交易的action,另一处在代码的最最底部新增一段:
...
//注意,这里交易的action没有声明为[[eosio::action]]
//所以不会出现在abi文件中,即表示这是非公开的
void transfer(const name from,const name to,const asset &quantity, const string memo);
...
extern "C"
{
//由于EOS有类似通知的功能,执行某些操作时,你可以指定通知给其他账号
//这里能接收所有的消息,入参
//`receiver`表示接收通知的账号
//`code`表示发出通知的合约账号
//`actin`表示发出通知的合约账号所被调用的action
void apply(uint64_t receiver, uint64_t code, uint64_t action) {
//校验,必须是`kofgametoken`合约的交易操作,才能执行以下的逻辑
if (code == name("kofgametoken").value && action == name("transfer").value) {
//把接收到的参数透传到我们自己定义的`transfer` action
//等价于捕捉到转账后,执行我们自己的`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);
}
}
接下来就是编写捕捉到转账后,需要执行的逻辑,我们把这块代码放在一个transfer
的action,其实就是把上一章的newgame
里面的代码,只是需要作一点修改:
ACTION kingofighter::transfer(const name from,const name to, const asset &quantity,const string memo) {
//这一句很重要,涉及到安全问题
//from == get_self() 的时候 return;表示当转账发起人是合约自身时,跳过
//to != get_self() 的时候return;表示接受这并不是合约自身时,跳过
//什么情况会出现to != get_self()?当其他合约发起通知的时候即:require_recipient操作,有兴趣可以查一下,这里不展开
if (from == get_self() || to != get_self()) return;
//2. 普通转账,无需执行逻辑
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. 解析备注
// 入参一共5个:action、user_seed、house_seed_hash、expire_timestamp、sig
vector<string> vec;
split_memo(vec, memo, ',');
if(vec.size() != 5)
return;
//5. 入参类型转换
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
,并不是拒绝调用者的请求;而是不执行下面的逻辑而已,转账依然是能成功的,但如果check
检查不通过,抛出了异常,转账也会失败。代码逻辑都写了注释这里不多说。
然后是修改battle
,需要新增挖矿的逻辑:
ACTION kingofighter::battle(const uint64_t& game_id,const string &house_seed) {
require_auth(get_self());
...
if (i & 1) {
//i为奇数,BOSS攻击
...
} else {
//i为偶数,玩家攻击
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。
现在,代码都已经编写完毕,但在开始对战之前,如果想要氪金,玩家还需要有SJ
,但目前玩家并没有任何SJ
代币,我们可以赠送一些给他用于内部测试,打开EOS studio,切换到Token智能合约界面,选择miner
,决定赠送500个SJ
给玩家:
上面注意需要使用小游戏的智能合约的账号权限来调用,执行成功后,可以看到右边
stat
表的发行量数据变为500
了。我们可以查看玩家这时候的 SJ
余额:玩家代币余额
一切准备就绪,我们来看看执行的效果。先启动服务端,不清楚的朋友请回到第三章。获取种子信息:
{
"house_seed_hash":"6e64e182e42920b689368ebc732188dbe7ac7c63939495f2671ad7d64937b0a9",
"expire_timestamp":1579072709,
"sig":"SIG_K1_Jx3ahMXUUxyEm2wYSXaJoXTsXhMGTC4g76dPxrEm58AF6gLEh3GVGSvWn4hDTq1bsKLeY1RfRAhfkzYz1wM6REChrWVLZi"
}
打开EOS studio,依然是切换到Token合约,选择transfer
action:
- 输入玩家的账号
- 输入小游戏的智能合约账号
- 输入氪金金额(注意格式)
- 备注,需要把入参拼接
根据我们获取到的种子信息,我们这里拼接到的数据是:
action:imrich,us:9a114079014a,ush:6e64e182e42920b689368ebc732188dbe7ac7c63939495f2671ad7d64937b0a9,et:1579072709,sig:SIG_K1_Jx3ahMXUUxyEm2wYSXaJoXTsXhMGTC4g76dPxrEm58AF6gLEh3GVGSvWn4hDTq1bsKLeY1RfRAhfkzYz1wM6REChrWVLZi
点击执行后,等一会服务端自动调用对战后,再看看gamerecords
表的对战结果,显示胜利。仔细查看每一次对BOSS造成的伤害明显增加了不少:
如果这时候你查看玩家的余额会发现还有
400
=500-200+100
,表示挖矿成功。或者你也可以通过日志查看整个执行过程,不过之前的日志是依赖history_plugin插件,好像是EOS1.3x后已经不建议使用,现在基本所有超级节点也都停用了,好在有一些平台提供了日志的服务,我自己使用的就是dfuse平台提供的接口。到这里,我们的小游戏全部逻辑已经搭建完成。一般会提供UI界面去给玩家操作,然后将攻击特效做得很炫酷,这时候玩家调用合约就没有那么方便,因为涉及到私钥签名,需要借助官方的
eosjs
js库,以及依赖scatter
。下一章是本系列的最终章,将会讲解如何实现EOS1.8版本的一个新型功能:
ONLY_BILL_FIRST_AUTHORIZER
,即如何让玩家不需要支付CPU和NET,就可以玩我们的小游戏。敬请期待:)
网友评论