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 이전 과 같음 */

좋은 웹페이지 즐겨찾기