CryptoZombies, 2-좀비가 희생물을 공격하다
챕터 1: 레슨2 개요
지난 레슨 1-좀비 공장 만들기에서는
좀비 이름을 입력받아 랜덤으로 좀비를 생성한 다음,
이 좀비를 블록체인상의 우리 앱 좀비 데이터베이스에 추가하는 함수를 만들었음
이번 레슨은 앱을 멀티플레이어 게임으로 만들기,
좀비를 랜덤으로만 생성하지 않고 좀 더 재미있는 방식으로 좀비를 생성하기
=> 새롭게 좀비를 생성하는 방법은 좀비가 다른 생명체를 "먹도록"해서!
좀비 먹이기
- 좀비가 먹이를 먹으면 바이러스에 감염됨
- 이 바이러스는 먹이를 새로운 좀비로 바꾸어 좀비 군대의 일원이 되도록 함
- 새로운 좀비의 DNA는 이전 좀비의 DNA와 먹이의 DNA를 활용하여 계산함
연습하기]
오른쪽에 보면 좀비가 먹이를 먹는 간단한 데모가 있지. 인간을 클릭해서 좀비가 먹이를 먹을 때 어떤 일이 일어나는지 보게!
새로운 좀비의 DNA는 원래 좀비의 DNA와 먹이의 DNA에 의해 결정된다는 것을 알 수 있지.
준비가 되면, "다음 챕터"를 클릭해서 계속 진행하게. 우리 게임을 멀티 플레이어 게임으로 만드는 것부터 시작하도록 하지.
==========
챕터 2: 매핑과 주소
데이터베이스에 저장된 좀비들에게 주인을 설정
mapping과 address라는 2가지 새로운 자료형 사용하기
주소
- 이더리움 블록체인 은행 계좌와 같은 계정들로 이루어져 있음
- 계정은 이더리움 블록체인상 통화인 ether 잔액
- 본인 계좌의 금액을 다른 계좌로 송금할 수 있음
- 각 계정은 은행 계좌 번호와 같은 주소를 가지고 있음 (ex, 0x0cE446255506E92DF41614C46F1d6df9Cc969183)
- 주소는 좀비들에 대한 소유권을 나타내는 고유 ID로 볼 수 있음
- 앱을 통해 새로운 좀비를 생성하면 좀비를 생성하면 좀비를 생성하는 함수를 호출한 이더리움(지갑)주소에 소유권 부여
매핑
- 솔리디티에서 구조화된 데이터를 저장하는 또 다른 방법
// 금융 앱용으로, 유저의 계좌 잔액을 보유하는 uint를 저장한다:
mapping (address => uint) public accountBalance;
// accountBalance[address] = uint;
// 혹은 userID로 유저 이름을 저장/검색하는 데 매핑을 쓸 수도 있다
mapping (uint => string) userIdToName;
// userIdToName[uint] = string;
- 키-값(key-value) 저장소로, 데이터를 저장하고 검색하는 데 이용됨
연습하기]
좀비 소유권을 저장하기 위해 2가지 매핑을 이용하고자 하네: 하나는 좀비 소유자의 주소를 추적하기 위한 것이고, 다른 하나는 소유한 좀비의 숫자를 추적하기 위한 것이네.
zombieToOwner라는 매핑을 생성한다. 키는 uint이고 (좀비 ID로 좀비를 저장하고 검색할 것이다), 값은 address이다. 이 매핑을 public으로 설정하자.
ownerZombieCount라는 매핑을 생성한다. 키는 address이고 값은 uint이다.
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
// 여기서 매핑 선언
mapping(uint => address) public zombieToOwner; // uint-zombieID => address-zombieAddr for tracking zombie owner
mapping(address => uint) ownerZombieCount; // address-Owner => uint-owned zombie count
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
==========
챕터 3: Msg.sender
_createZombie 메소드 업데이트
msg.sender
- 모든 함수에서 이용 가능한 특정 전역 변수
- 그 중 하나가 현재 함수를 호출한 사람(혹은 스마트 컨트랙트)의 주소를 가리키는 msg.sender
- msg.sender를 이용하고 mapping을 업데이트하는 예시
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// `msg.sender`에 대해 `_myNumber`가 저장되도록 `favoriteNumber` 매핑을 업데이트한다 `
favoriteNumber[msg.sender] = _myNumber;
// ^ 데이터를 저장하는 구문은 배열로 데이터를 저장할 떄와 동일하다
}
function whatIsMyNumber() public view returns (uint) {
// sender의 주소에 저장된 값을 불러온다
// sender가 `setMyNumber`을 아직 호출하지 않았다면 반환값은 `0`이 될 것이다
return favoriteNumber[msg.sender];
}
- msg.sender를 활용하면 이더리움 블록체인의 보안성을 이용할 수 있음
연습하기]
레슨 1에서 다뤘던 _createZombie 메소드를 업데이트하여 이 함수를 호출하는 누구나 좀비 소유권을 부여하도록 해 보세.
먼저, 새로운 좀비의 id가 반환된 후에 zombieToOwner 매핑을 업데이트하여 id에 대하여 msg.sender가 저장되도록 해보자.
그 다음, 저장된 msg.sender을 고려하여 ownerZombieCount를 증가시키자.
자바스크립트와 마찬가지로 솔리디티에서도 uint를 ++로 증가시킬 수 있다.
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
// 여기서 시작
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
==========
챕터 4: Require
createRandomZombie 무제한으로 호출하는 것을 방지
- 좀비 이름 입력에 따라 새로운 좀비를 생성할 수 있음
- 만약 함수를 계속 호출해서 무제한으로 좀비를 생성한다면, 문제가 생길 수도 있음
- 각 플레이어가 이 함수를 한 번만 호출할 수 있도록 설정하려면 require 활용해서 구현 가능
- require는 특정 조건이 참이 아닐 때 함수가 에러 메시지를 발생하고 실행을 멈추게 함
function sayHiToVitalik(string _name) public returns (string) {
// _name이 "Vitalik"인지 비교한다. 참이 아닐 경우 에러 메시지를 발생하고 함수를 벗어난다
// (참고: 솔리디티는 고유의 스트링 비교 기능을 가지고 있지 않기 때문에
// 스트링의 keccak256 해시값을 비교하여 스트링 값이 같은지 판단한다)
require(keccak256(_name) == keccak256("Vitalik"));
// 참이면 함수 실행을 진행한다:
return "Hi!";
}
연습하기]
우리의 좀비 게임에서 유저가 createRandomZombie 함수를 반복적으로 호출해서 자신의 군대에 좀비를 무제한으로 생성하는 것을 원하지 않네. 그렇게 되면 게임이 재미없게 될 걸세.
require를 활용하여 유저들이 첫 좀비를 만들 때 이 함수가 유저 당 한 번만 호출되도록 해 보세.
require 키워드를 createRandomZombie 앞부분에 입력한다. require 함수가 ownerZombieCount[msg.sender]이 0과 같은지 확인하도록 하고, 0이 아닌 경우 에러 메시지를 출력하도록 한다.
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
// 여기서 시작
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
==========
챕터 5: 상속
하나의 긴 컨트랙트를 만들기 보다, 여러 컨트랙트에 코드 로직을 나누는 것이 합리적임
contract Doge {
function catchphrase() public returns (string) {
return "So Wow CryptoDoge";
}
}
// BabyDog 컨트랙트를 컴파일해서,
// catchphrase() 함수와 anotherCatchphrase() 함수 모두 접근 가능
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string) {
return "Such Moon BabyDoge";
}
}
연습하기]
다음 챕터에서 우리 좀비들이 먹이를 먹고 번식하도록 하는 기능을 구현할 것일세. 그 기능의 로직을 ZombieFactory의 모든 메소드를 상속하는 클래스에 넣어 보도록 하세.
ZombieFactory 아래에 ZombieFeeding 컨트랙트를 생성한다. 이 컨트랙트는 ZombieFactory를 상속해야 한다.
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
// 여기서 시작
contract ZombieFeeding is ZombieFactory {
//
}
==========
챕터 6: Import
여러 sol 파일에 나누기
- 다수의 파일이 있고, 어떤 파일을 다른 파일로 불러오고자 할때, import 키워드 사용
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
연습하기]
다수의 파일이 있는 구조를 갖추었으니 import를 활용하여 다른 파일의 내용을 읽어올 필요가 있네.
새로운 파일 zombiefeeding.sol에 zombiefactory.sol를 불러 온다(import).
/* zombiefeeding.sol */
pragma solidity ^0.4.19;
// 여기에 import 구문을 넣기
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
}
/* zombiefactory.sol */
pragma solidity ^0.4.19;
contract ZombieFactory {
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
zombieToOwner[id] = msg.sender;
ownerZombieCount[msg.sender]++;
NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
==========
챕터 7: Storage vs Memory
변수 저장 공간(storage, memory)
- Storage는 블록체인 상에 영구적으로 저장되는 변수를 의미 (HDD,SDD와 유사)
- Memory는 임시적으로 저장되는 변수로, 컨트랙트 함수에 대한 외부 호출이 일어나면 지워짐 (RAM과 유사)
- 대부분의 경우 솔리디티가 알아서 처리함
- 상태 변수는 default로 storage로 선언되어 블록체인에 영구적으로 저장됨
- 함수 내에 선언된 변수는 memory로 자동 선언되어 함수 호출이 종료되면 사라짐
- 키워드(storage, memory)를 사용해야할 때는 함수 내의 구조체와 배열을 처리하는 경우
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// ^ 꽤 간단해 보이나, 솔리디티는 여기서
// `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!";
// ...이 코드는 임시 변수인 `anotherSandwich`를 변경하는 것으로
// `sandwiches[_index + 1]`에는 아무런 영향을 끼치지 않는다. 그러나 다음과 같이 코드를 작성할 수 있다:
sandwiches[_index + 1] = anotherSandwich;
// ...이는 임시 변경한 내용을 블록체인 저장소에 저장하고자 하는 경우이다.
}
}
연습하기]
먹이를 먹고 번식하는 능력을 우리 좀비들에게 부여할 시간이네!
좀비가 어떤 다른 생명체를 잡아 먹을 때, 좀비 DNA가 생명체의 DNA와 혼합되어 새로운 좀비가 생성될 것이네.
feedAndMultiply라는 함수를 생성한다. 이 함수는 uint형인 _zombieId 및 _targetDna을 전달받는다. 이 함수는 public으로 선언되어야 한다.
다른 누군가가 우리 좀비에게 먹이를 주는 것을 원치 않는다. 그러므로 주인만이 좀비에게 먹이를 줄 수 있도록 한다. require 구문을 추가하여 msg.sender가 좀비 주인과 동일하도록 한다. (이는 createRandomZombie 함수에서 쓰인 방법과 동일하다)
참고: 다시 말하지만, 우리가 작성한 확인 기능은 기초적이기 때문에 컴파일러는 msg.sender가 먼저 나올 것을 기대하고, 항의 순서를 바꾸면 잘못된 값이 입력되었다고 할 걸세. 하지만 보통 코드를 작성할 때 항의 순서는 자네가 원하는 대로 정하면 되네. 어떤 경우든 참이 되거든.
먹이를 먹는 좀비 DNA를 얻을 필요가 있으므로, 그 다음으로 myZombie라는 Zombie형 변수를 선언한다 (이는 storage 포인터가 될 것이다). 이 변수에 zombies 배열의 _zombieId 인덱스가 가진 값에 부여한다.
/* zombiefeeding.sol */
pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
// 여기서 시작
function feedAndMultiply (uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
}
}
/* zombiefactory.sol 이전 과 같음 */
Author And Source
이 문제에 관하여(CryptoZombies, 2-좀비가 희생물을 공격하다), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@johnnyji/CryptoZombies-2-좀비가-희생물을-공격하다저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)