美文网首页区块链@IT·互联网区块链大学
4-从零开发EOS区块链小游戏系列 - 加入Token体系

4-从零开发EOS区块链小游戏系列 - 加入Token体系

作者: Jan_gogogo | 来源:发表于2020-01-15 19:19 被阅读0次

    目录

     Token并非区块链独有,在区块链世界体现为一种权益证明。而且在不同的应用场景也叫法也可以不同,可以是票证、股份、代币。而在EOS中,代币符号就叫做EOS,代币可以交易,可以购买内存、和计算网络资源,如果次有代币还可以拥有投票权。
     本章我们为自己的小游戏建立自己的Token,下面我们称为代币,而代币的符号(symbol)就叫SJ。上面说了EOS(后面红字EOS统一指代币)就是代币,从技术角度来看,代币没有什么特殊的,他就是在一个智能合约里面一张表的记录而已,所以要有代币必先有合约账号,EOS也不例外,他的合约账号是:eosio.token;如果你拥有EOS,那么在eosio.tokenaccounts这张表就会有你的记录,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.tokenhpp和cpp文件的代码复制复制到你的合约里面,这时候目录应该是这样:


     然后打开你的.hpp文件,把以下这一行修改一下:
    class [[eosio::contract("改为你的合约账号")]] token : public contract 
    

     接着打开.cpp文件,

    #include <eosio.token.hpp>
    改为
    #include <你的合约账号.hpp> 
    //其实就是上面目录结构图include文件里面的hpp文件名
    

     最后就是编译-》部署到麒麟测试链,你还需要为合约购买内存和CPU,这些在第一章都有说,这里就不重复了。
     现在我们已经将合约代码部署到区块链了,接着就是创建我们的代币,打开EOS studio,切换到代币的合约界面,选择create action:

    创建token
     入参:
    • issuser:指定代币发行人账号,为什么这里指定游戏的合约账号呢?我们下面会说。
    • maximum_supply:最大发行量为100万,EOS发行量是100亿,表示我们的代币还是比较稀有的。
       第三个参入是调用权限,需要填写本合约的账号,因为create校验了必须本合约权限调用,这是代码确定的:
    void token::create( const name&   issuer,
                        const asset&  maximum_supply )
    {
        require_auth( _self ); //必须本合约账号权限
       ...
    }
    

     完成后,可以在界面右边看到数据,SUPPLY值为0,表示目前还没有发行任何代币:

    代币创建成功

     以上,我们的代币已经就绪,但是什么时候应该发行代币?不发行就没有交易,没有流通也就没有价值。回到我们的小游戏,还记得游戏逻辑是如果玩家胜利了,会奖励一定的SJ,这里其实就是一个发行的过程,也可以叫做挖矿。所以我们需要修改一下小游戏合约的代码,让他和我们的代币结合起来使用。

    加入token后的完整流程图

     留意上图,对比上一章的流程,新增了两个功能,一个是第三步的可支持氪金以及发行代币;氪金逻辑:我们决定10个SJ(代币的符号,在游戏里代表水晶。无特殊说明下面SJ均表示代币)可以提高1点攻击力,这样就可以提高玩家的胜率。那这里就涉及到转账需求了,如果想氪金,玩家就要从自己的账号转账给小游戏的账号,回想上一章,开始一局游戏时,玩家调用了合约的newgameaction,最开始的想法是可不可以调用的同时,附带转账功能呢?ETH就是这样设计的,但EOS这里不行。
     至于EOS为什么不行?我个人的理解是:ETHEOS的设计不同,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:

    transfer调用
    1. 输入玩家的账号
    2. 输入小游戏的智能合约账号
    3. 输入氪金金额(注意格式)
    4. 备注,需要把入参拼接

    根据我们获取到的种子信息,我们这里拼接到的数据是:

    action:imrich,us:9a114079014a,ush:6e64e182e42920b689368ebc732188dbe7ac7c63939495f2671ad7d64937b0a9,et:1579072709,sig:SIG_K1_Jx3ahMXUUxyEm2wYSXaJoXTsXhMGTC4g76dPxrEm58AF6gLEh3GVGSvWn4hDTq1bsKLeY1RfRAhfkzYz1wM6REChrWVLZi
    

    点击执行后,等一会服务端自动调用对战后,再看看gamerecords表的对战结果,显示胜利。仔细查看每一次对BOSS造成的伤害明显增加了不少:


     如果这时候你查看玩家的余额会发现还有400=500-200+100,表示挖矿成功。或者你也可以通过日志查看整个执行过程,不过之前的日志是依赖history_plugin插件,好像是EOS1.3x后已经不建议使用,现在基本所有超级节点也都停用了,好在有一些平台提供了日志的服务,我自己使用的就是dfuse平台提供的接口。
     到这里,我们的小游戏全部逻辑已经搭建完成。一般会提供UI界面去给玩家操作,然后将攻击特效做得很炫酷,这时候玩家调用合约就没有那么方便,因为涉及到私钥签名,需要借助官方的eosjsjs库,以及依赖scatter
     下一章是本系列的最终章,将会讲解如何实现EOS1.8版本的一个新型功能:ONLY_BILL_FIRST_AUTHORIZER,即如何让玩家不需要支付CPU和NET,就可以玩我们的小游戏。敬请期待:)

    本章节源代码地址:https://github.com/jan-gogogo/kof-chapter4

    相关文章

      网友评论

        本文标题:4-从零开发EOS区块链小游戏系列 - 加入Token体系

        本文链接:https://www.haomeiwen.com/subject/zdhmactx.html