Solidity-僵尸攻击人类

原教程:https://cryptozombies.io/zh/lesson/2/chapter/1

第1章:第二课概览

在第一课中,我们创建了一个函数用来生成僵尸,并且将它放入区块链上的僵尸数据库中。在第二课里,我们会让我们的 app 看起来更像一个游戏: 它得支持多用户,并且采用更加有趣,而不仅仅使用随机的方式,来生成新的僵尸。

如何生成新的僵尸呢?通过让现有的僵尸猎食其他生物!

僵尸猎食

僵尸猎食的时候,僵尸病毒侵入猎物,这些病毒会将猎物变为新的僵尸,加入你的僵尸大军。系统会通过猎物和猎食者僵尸的DNA计算出新僵尸的DNA。

僵尸最喜欢猎食什么物种呢? 等你学完第二课就知道了!

第2章:映射(Mapping)和地址(Address)

我们通过给数据库中的僵尸指定“主人”, 来支持“多玩家”模式。

如此一来,我们需要引入2个新的数据类型:mapping(映射)address(地址)

Addresses (地址)

以太坊区块链由 _ account _(账户)组成,你可以把它想象成银行账户。一个帐户的余额是 _以太_ (在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样。

每个帐户都有一个“地址”,你可以把它想象成银行账号。这是账户唯一的标识符,它看起来长这样:

0x0cE446255506E92DF41614C46F1d6df9Cc969183

(这是 CryptoZombies 团队的地址,如果你喜欢 CryptoZombies 的话,请打赏我们一些以太币!😉)

我们将在后面的课程中介绍地址的细节,现在你只需要了解地址属于特定用户(或智能合约)的。

所以我们可以指定“地址”作为僵尸主人的ID。当用户通过与我们的应用程序交互来创建新的僵尸时,新僵尸的所有权被设置到调用者的以太坊地址下。

Mapping(映射)

在第1课中,我们看到了 _ 结构体 __ 数组 __映射_是另一种在 Solidity 中存储有组织数据的方法。

映射是这样定义的:

//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中:
mapping (address => uint) public accountBalance;
//或者可以用来通过userId 存储/查找的用户名
mapping (uint => string) userIdToName;

映射本质上是存储和查找数据所用的键-值对。在第一个例子中,键是一个 address,值是一个 uint,在第二个例子中,键是一个uint,值是一个 string。

第3章:Msg.sender

现在有了一套映射来记录僵尸的所有权了,我们可以修改_createZombie方法来运用它们。

为了做到这一点,我们要用到msg.sender

msg.sender

Solidity中,有一些全局变量可以被所有函数调用。其中一个就是msg.sender,它指的是当前调用者(或智能合约)的address

注意:在 Solidity 中,功能执行始终需要从外部调用者开始。一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以msg.sender总是存在的。

以下是使用msg.sender来更新mapping的例子:

mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {
  // 更新我们的 `favoriteNumber` 映射来将 `_myNumber`存储在 `msg.sender`名下
  favoriteNumber[msg.sender] = _myNumber;
  // 存储数据至映射的方法和将数据存储在数组相似
}

function whatIsMyNumber() public view returns (uint) {
  // 拿到存储在调用者地址名下的值
  // 若调用者还没调用 setMyNumber, 则值为 `0`
  return favoriteNumber[msg.sender];
}

在这个小小的例子中,任何人都可以调用setMyNumber在我们的合约中存下一个uint并且与他们的地址相绑定。然后,他们调用whatIsMyNumber就会返回他们存储的uint

使用msg.sender很安全,因为它具有以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。

第4章:Require

在第一课中,我们成功让用户通过调用createRandomZombie函数并输入一个名字来创建新的僵尸。但是,如果用户能持续调用这个函数来创建出无限多个僵尸加入他们的军团,这游戏就太没意思了!

于是,我们作出限定:每个玩家只能调用一次这个函数。这样一来,新玩家可以在刚开始玩游戏时通过调用它,为其军团创建初始僵尸。

我们怎样才能限定每个玩家只调用一次这个函数呢?

答案是使用require。require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行:

function sayHiToVitalik(string _name) public returns (string) {
	// 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
    // (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较两字符串的keccak256哈希值来进行判断)
    require(keccak256(_name) == keccak256("Vitalik"));
    // 如果返回 true, 运行如下语句
    return "Hi!";
}

如果你这样调用函数sayHiToVitalik(“Vitalik”),它会返回“Hi!”。而如果调用的时候使用了其他参数,它则会抛出错误并停止执行。

因此,在调用一个函数之前,用require验证前置条件是非常有必要的。

第5章:继承(Inheritance)

我们的游戏代码越来越长。 当代码过于冗长的时候,最好将代码和逻辑分拆到多个不同的合约中,以便于管理。

有个让Solidity的代码易于管理的功能,就是合约inheritance (继承)

contract Doge {
    function catchphrase() public returns (string) {
        return "So Wow CryptoDoge";
    }
}

contract BabyDoge is Doge {
    function anotherCatchphrase() public returns (string) {
        return "Such Moon BabyDoge";
    }
}

由于BabyDoge是从Doge那里inherits(继承)过来的。这意味着当你编译和部署了BabyDoge,它将可以访问catchphrase()anotherCatchphrase()和其他我们在Doge中定义的其他公共函数。

这可以用于逻辑继承(比如表达子类的时候,Cat是一种Animal)。但也可以简单地将类似的逻辑组合到不同的合约中以组织代码。

第6章:引入(Import)

哇!你有没有注意到,我们只是清理了下右边的代码,现在你的编辑器的顶部就多了个选项卡。 尝试点击它的标签,看看会发生什么吧!

代码已经够长了,我们把它分成多个文件以便于管理。 通常情况下,当Solidity项目中的代码太长的时候我们就是这么做的。

Solidity中,当你有多个文件并且想把一个文件导入另一个文件时,可以使用import语句:

import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

这样当我们在合约(contract)目录下有一个名为someothercontract.sol的文件(./ 就是同一目录的意思),它就会被编译器导入。

第7章:Storage与Memory

在 Solidity 中,有两个地方可以存储变量 —— storagememory

Storage变量是指永久存储在区块链中的变量。Memory变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。

大多数时候你都用不到这些关键字,默认情况下Solidity会自动处理它们。状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。

然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的_ 结构体 __ 数组 _时:

contract SandwichFactory {
    struct Sandwich {
    	string name;
    	string status;
	}

    Sandwich[] sandwiches;

    function eatSandwich(uint _index) public {
        // Sandwich mySandwich = sandwiches[_index];

    	// ^ 看上去很直接,不过 Solidity 将会给出警告
    	// 告诉你应该明确在这里定义 `storage` 或者 `memory`。

    	// 所以你应该明确定义 `storage`:
    	Sandwich storage mySandwich = sandwiches[_index];
    	// ...这样 `mySandwich` 是指向 `sandwiches[_index]`的指针
    	// 在存储里,另外...
    	mySandwich.status = "Eaten!";
    	// ...这将永久把 `sandwiches[_index]` 变为区块链上的存储

    	// 如果你只想要一个副本,可以使用`memory`:
    	Sandwich memory anotherSandwich = sandwiches[_index + 1];
    	// ...这样 `anotherSandwich` 就仅仅是一个内存里的副本了
    	// 另外
    	anotherSandwich.status = "Eaten!";
    	// ...将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响
    	// 不过你可以这样做:
    	sandwiches[_index + 1] = anotherSandwich;
    	// ...如果你想把副本的改动保存回区块链存储
  	}
}

如果你还没有完全理解究竟应该使用哪一个,也不用担心 —— 在本教程中,我们将告诉你何时使用storage或是memory,并且当你不得不使用到这些关键字的时候,Solidity编译器也发警示提醒你的。

现在,只要知道在某些场合下也需要你显式地声明storagememory就够了!

第8章:僵尸的DNA

我们来把feedAndMultiply函数写完吧。

获取新的僵尸DNA的公式很简单:计算猎食僵尸DNA和被猎僵尸DNA之间的平均值。

例如:

function testDnaSplicing() public {
    uint zombieDna = 2222222222222222;
    uint targetDna = 4444444444444444;
    uint newZombieDna = (zombieDna + targetDna) / 2;
    // newZombieDna 将等于 3333333333333333
}

以后,我们也可以让函数变得更复杂些,比方给新的僵尸的 DNA 增加一些随机性之类的。但现在先从最简单的开始 —— 以后还可以回来完善它嘛。

第9章:更多关于函数可见性

我们上一课的代码有问题!

编译的时候编译器就会报错。

错误在于,我们尝试从ZombieFeeding中调用_createZombie函数,但_createZombie却是ZombieFactoryprivate (私有)函数。这意味着任何继承自ZombieFactory的子合约都不能访问它。

internal 和 external

publicprivate属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal(内部)external(外部)

internalprivate类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。(嘿,这听起来正是我们想要的那样!)。

externalpublic类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。稍后我们将讨论什么时候使用externalpublic

声明函数internalexternal类型的语法,与声明privatepublic类型相同:

contract Sandwich {
    uint private sandwichesEaten = 0;

    function eat() internal {
    	sandwichesEaten++;
    }
}

contract BLT is Sandwich {
    uint private baconSandwichesEaten = 0;

    function eatWithBacon() public returns (string) {
    	baconSandwichesEaten++;
    	// 因为eat() 是internal 的,所以我们能在这里调用
    	eat();
    }
}

第10章:僵尸吃什么?

是时候让我们的僵尸去捕猎! 那僵尸最喜欢的食物是什么呢?

Crypto 僵尸喜欢吃的是…

CryptoKitties! 😱😱😱

(正经点,我可不是开玩笑😆)

为了做到这一点,我们要读出CryptoKitties智能合约中的kittyDna。这些数据是公开存储在区块链上的。区块链是不是很酷?

别担心 —— 我们的游戏并不会伤害到任何真正的CryptoKitty。 我们只读取CryptoKitties数据,但却无法在物理上删除它。

与其他合约的交互

如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个interface (接口)

先举一个简单的栗子。 假设在区块链上有这么一个合约:

contract LuckyNumber {
    mapping(address => uint) numbers;

    function setNum(uint _num) public {
    	numbers[msg.sender] = _num;
    }

    function getNum(address _myAddress) public view returns (uint) {
    	return numbers[_myAddress];
    }
}

这是个很简单的合约,您可以用它存储自己的幸运号码,并将其与您的以太坊地址关联。 这样其他人就可以通过您的地址查找您的幸运号码了。

现在假设我们有一个外部合约,使用getNum函数可读取其中的数据。

首先,我们定义LuckyNumber合约的interface

contract NumberInterface {
    function getNum(address _myAddress) public view returns (uint);
}

请注意,这个过程虽然看起来像在定义一个合约,但其实内里不同:

首先,我们只声明了要与之交互的函数 —— 在本例中为getNum—— 在其中我们没有使用到任何其他的函数或状态变量。

其次,我们并没有使用大括号({})定义函数体,我们单单用分号(;)结束了函数声明。这使它看起来像一个合约框架。

编译器就是靠这些特征认出它是一个接口的。

在我们的app代码中使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。

在下一课中,我们将真正调用其他合约的函数。目前我们只要声明一个接口,用于调用 CryptoKitties 合约就行了。

第11章:使用接口

继续前面NumberInterface的例子,我们既然将接口定义为:

contract NumberInterface {
    function getNum(address _myAddress) public view returns (uint);
}

我们可以在合约中这样使用:

contract MyContract {
    address NumberInterfaceAddress = 0xab38...;
    // ^ 这是FavoriteNumber合约在以太坊上的地址
    NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
    // 现在变量 `numberContract` 指向另一个合约对象

    function someFunction() public {
    	// 现在我们可以调用在那个合约中声明的 `getNum`函数:
    	uint num = numberContract.getNum(msg.sender);
    	// ...在这儿使用 `num`变量做些什么
    }
}

通过这种方式,只要将您合约的可见性设置为public(公共)external(外部),它们就可以与以太坊区块链上的任何其他合约进行交互。

第12章:处理多返回值

getKitty是我们所看到的第一个返回多个值的函数。我们来看看是如何处理的:

function multipleReturns() internal returns(uint a, uint b, uint c) {
    return (1, 2, 3);
}

function processMultipleReturns() external {
    uint a;
    uint b;
    uint c;
    // 这样来做批量赋值:
    (a, b, c) = multipleReturns();
}

// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {
    uint c;
    // 可以对其他字段留空:
    (,,c) = multipleReturns();
}

第13章: 奖励: Kitty 基因

我们的功能逻辑主体已经完成了…现在让我们来添一个奖励功能吧。

这样吧,给从小猫制造出的僵尸添加些特征,以显示他们是猫僵尸。

要做到这一点,咱们在新僵尸的DNA中添加一些特殊的小猫代码。

还记得吗,第一课中我们提到,我们目前只使用16位DNA的前12位数来指定僵尸的外观。所以现在我们可以使用最后2个数字来处理“特殊”的特征。

这样吧,把猫僵尸DNA的最后两个数字设定为99(因为猫有9条命)。所以在我们这么来写代码:如果这个僵尸是一只猫变来的,就将它DNA的最后两位数字设置为99。

if 语句

if语句的语法在 Solidity 中,与在 JavaScript 中差不多:

function eatBLT(string sandwich) public {
    // 看清楚了,当我们比较字符串的时候,需要比较他们的 keccak256 哈希码
    if (keccak256(sandwich) == keccak256("BLT")) {
    	eat();
    }
}

第14章: 放在一起

至此,你已经学完第二课了!

查看下→_→的演示,看看他们怎么运行起来得吧。继续,你肯定等不及看完这一页😉。点击小猫,攻击!看到你斩获一个新的小猫僵尸了吧!

JavaScript 实现
我们只用编译和部署ZombieFeeding,就可以将这个合约部署到以太坊了。我们最终完成的这个合约继承自ZombieFactory,因此它可以访问自己和父辈合约中的所有public方法。

我们来看一个与我们的刚部署的合约进行交互的例子, 这个例子使用了JavaScriptweb3.js

var abi = /* abi generated by the compiler */
var ZombieFeedingContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Ethereum after deploying */
var ZombieFeeding = ZombieFeedingContract.at(contractAddress)

// 假设我们有我们的僵尸ID和要攻击的猫咪ID
let zombieId = 1;
let kittyId = 1;

// 要拿到猫咪的DNA,我们需要调用它的API。这些数据保存在它们的服务器上而不是区块链上。
// 如果一切都在区块链上,我们就不用担心它们的服务器挂了,或者它们修改了API,
// 或者因为不喜欢我们的僵尸游戏而封杀了我们
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {
    let imgUrl = data.image_url
    // 一些显示图片的代码
})

// 当用户点击一只猫咪的时候:
$(".kittyImage").click(function(e) {
    // 调用我们合约的 `feedOnKitty` 函数
    ZombieFeeding.feedOnKitty(zombieId, kittyId)
})

// 侦听来自我们合约的新僵尸事件好来处理
ZombieFactory.NewZombie(function(error, result) {
    if (error) return
    // 这个函数用来显示僵尸:
    generateZombie(result.zombieId, result.name, result.dna)
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值