Project #3 - JMT(메이플스토리st 웹3 NFT게임) 회고

김도영·2022년 10월 4일
0
post-thumbnail

🚩 팀명 : JMT(Joyful Maplystory NFT)

📆 프로젝트 기간 (8/31~9/29)

📃 프로젝트 소개

이 프로젝트는 ERC20, ERC721, ERC1155를 모두 활용하고 메이플스토리st를 적용한 Web3 기반 게임 개발이다.

메이플스토리는 2003년 부터 계속 파생 서비스를 런칭, 확장해오고 있으며, 블록체인과 메타버스까지 바라보고 있습니다. 저희 JMT팀은 NFT와 디파이를 활용하여 메이플스토리를 블록체인 Web3 게임으로 개발을 시도해보았다.


📝 역할 분담

나는 전투와 관련된 것들을 구현하였다. 전투 front부터 시작해서, 전투 컨트랙트, 전투 이미지 구현, 그리고 전투 결과에 따른 랜덤의 JMT 토큰과 강화 스크롤을 컨트랙트로 작성하고 API를 구현하였다.


✅ Polygon 채택 이유

게임 컨텐츠에는 트랜잭션 발생 빈도가 높다. 이러한 상황에서 기존에 많이 쓰이던 이더리움 네트워크는 높은 가스비로 인해 사용자에게 부담이 느껴질 수 있다. 폴리곤은 이더리움의 확장성과 보안성을 높이기 위해 구축된 대표적인 Layer2이다. 따라서, 비교적 저렴한 가스비와 빠른 처리속도가 강점이라 게임 서비스에 적용하기에 최적이기 때문에 폴리곤을 채택했다.


✅ 토큰 이코노미

JMT 토큰

JMT 토큰으로 게임에 사용되는 모든 컨텐츠에 유통되고 소비됩니다. 주로 무기 및 캐릭터 뽑기, 장비 강화에 쓰이고 랭킹과 전투에 승리 시 획득 가능하다.

vJMT 토큰

vJMT 토큰으로 JMT가 소비되는 과정에서 1%의 tx가 발생하는데 이에 해당하는 Commission을 vJMT 토큰으로 보상을 줍니다. 또한, 사용자는 게임에서 소비한 JMT의 수수료 만큼 vJMT 토큰을 받을 수 있다.


✅ 기술 스택


✅ 구현 목표

구현 목표는 Bare-minimum을 잡고 확장하는 식으로 구현하였다.

Front-end

  • 웹 페이지에 처음 접속했을때 MetaMask Login페이지
  • (JMT 토큰이 없을 경우) Matic <-> JMT토큰 스왑페이지
  • (JMT 토큰이 있을 경우) 첫 캐릭터 & 무기 민팅 페이지
  • 메인화면을 보여주는 Home 화면
  • 내 캐릭터와 무기가 있는 Inventory 페이지
  • Inventory에서 무기를 강화할 수 있는 기능
  • NFT캐릭터와 무기를 거래 할 수 있는 NFT 마켓 페이지
  • 유저 순위가 보이는 랭킹 페이지
  • 토큰 전송, 유동성 풀 제공, 토큰 스왑, 스테이킹을 할 수 있는 토큰 스왑 페이지
  • 전투를 할 수 있는 전투 페이지
  • 전투가 종료된 후 보상을 수령할 수 있는 페이지

Back-end

  • Mysql Sequelize로 DB 관리
  • Node.js Express로 서버 연동
  • 메이플스토리st 관련 script 데이터
  • ERC20 기반 토큰 컨트랙트, ERC721 기반 캐릭터 컨트랙트, ERC1155 기반 무기 관련 컨트랙트
  • 민팅 컨트랙트
  • 토큰 & 스왑 & 스테이킹 컨트랙트
  • 전투 컨트랙트
  • 각 컨트랙트에 관련된 API들

💻 기능 소개

메타마스크 로그인 화면

(JMT 토큰이 없을 경우) 첫 토큰 스왑 페이지

JMT 토큰이 없을 경우 Matic을 통하여 JMT 토큰으로 스왑할 수 있는 토큰 스왑 페이지가 나온다.

(JMT 토큰이 있을 경우) 첫 캐릭터 & 무기 민팅 페이지

로그인한 유저가 JMT 토큰이 있을 경우 첫 캐릭터와 무기를 민팅할 수 있는 페이지가 나온다. 유저의 캐릭터, 무기 장착 정보는 유저 컨트랙트와 db에 저장됩니다. 유저이름을 입력 후 민팅이 시작되며, 캐릭터, 첫 무기, 장착, 총 세가지 컨트랙트에 트랜잭션이 발생한다.

첫 민팅 후 홈 화면

민팅이 완료 된 후 홈 화면으로 넘어가게 된다.

인벤토리

인벤토리 페이지로 가면 내가 소유한 캐릭터와 무기 및 강화 스크롤을 확인할 수 있고, 장착도 할 수 있다.장착 변경시, 유저 컨트랙트와 db에 정보가 업데이트 되며, 홈에서 바로 반영이 되는 모습을 확인할 수 있다.

무기 강화

소유 무기는 강화서로 레벨을 높일 수 있습니다.
전투 후 보상 받은 강화서를 사용하면 강화서 확률에 따라,
강화가 실패할 수 있고,
강화 성공시, 다음 레벨의 무기가 지급되며, 기존 무기는 BURN됩니다.

상점

상점의 구매 탭에서 등록된 NFT 캐릭터들을 구매할 수 있다. 구매가 성사될 경우, 구매 금액의 90퍼센트는 판매자, 10퍼센트는 수수료로 재무 지갑으로 이동한다. 그 중 반은 burn 된다.

상점의 판매 탭에서 내가 소유한 NFT 캐릭터들을 판매/판매 취소 할 수 있다

캐릭터와 마찬가지로 무기 또한 판매, 판매 취소, 구매를 할 수 있다.

NFT 뽑기

상점의 뽑기 탭에서 NFT 캐릭터를 뽑을 수 있다.

마찬가지로 NFT 무기도 뽑을 수 있다.

랭킹

랭킹은 실제 무기 소유여부 확인 후 레벨에 따라 유저를 정렬한다. 상위 랭커는 재무 지갑에 쌓이는 수수료의 10%를 랭킹에 따라 보상으로 지급 받을 수 있다. 랭커를 클릭하면 폴리곤 스캔이 떠 해당 유저의 트랜잭션 히스토리를 확인할 수 있다.

전투

fight 버튼을 클릭하면 전투가 시작된다다. ‘유저 컨트랙트’에 의해 상대방이 랜덤으로 매칭되며, 전투를 시작하면 ‘전투 컨트랙트’에 의해 승, 패가 결정 되고 결과를 확인할 수 있는 버튼이 나온다.

전투 보상

전투 결과 확인시, 유저가 승리 하면, 보상을 수령할 수 있다. 이 때, 보상은 1JMT 토큰과 무기 강화서가 확률에 의해 지급된다.

토큰 스왑

토큰 스왑은 CFMM 계산 방식유니스왑과 같은 방식으로 동작한다. 스시 스왑처럼 MATIC에서 JMT 스왑시 커미션을 vJMT토큰으로 지급 받는다.

유동성 풀 제공

유동성 제공은 MATIC : JMT를 마켓 유동성에 비례하여 추가할 수 있으며 실시간으로 vJMT를 보상으로 클레임 할수있다. 유니스왑과 같은 CFMM  고정 상수 방식을 사용 현재 컨트렉트 Reserve 비율에맞는 토큰 갯수를 가져와서 Desposite하고 LPtoken을 발행 LPtoken에 비례한 vJMT 토큰을 보상으로 받을 수 있다.

스테이킹

앞선 DeFi 과정을 통해 얻은 vJMT 토큰을 스테이킹 할수 있다. 보상은 실시간으로 수령 가능하며, unStaking시 타임락 3일 후 JMT를 수령할 수 있다.  


❗ 개발 이슈

  1. 프로젝트3를 진행하는 과정에서 팀원 한 분이 블록체인 트랜잭션이 발생하기 전에 모달로 알림창을 띄우는 것을 구현하는 과정에서 정보가 갱신이 되지 않는 문제가 발생하였다. 당일날까지 구현을 해야 다음 작업으로 넘어 갈수 있는 상황 이였기에 팀원분은 도움을 요청하셨고, 나는 이를 확인하고 팀원분에게 구글밋으로 몇 가지 해결 방안을 제시하였다. 팀원분은 이 해결 방안으로 문제를 해결하였고, 다음 작업을 무사히 진행할 수 있었다.

  2. 전투 컨트랙트를 구현하는 과정에서 확률을 다루는 요소가 많았는데, '<=', '>='를 사용하면 결국 2가지의 일을 처리하는 것이라 가스비가 많이 발생하였다.

function setFight(address _addr, uint _userstrength, uint _matchingstrength) public {
        require( msg.sender == _addr, "Incorrect Address");
        uint rand = randMod(100);
        if ( _userstrength > _matchingstrength ) {
            result = "User Win!!!";
        } else if ( rand <= rewardProbability ) { // 10%의 확률로 유저에게 크리티컬 발동
            _userstrength = _matchingstrength + 1;
            result = (_userstrength >= _matchingstrength? "User dramatic Win !!": "User Lose...");
        } else if (_userstrength == _matchingstrength) {  // 비겼을 때, 50%확률로 승리
            result = ( rand <= rewardProbability2 ? "User dramatic Win !!": "User Lose...");
        } else {
            result = "User Lose...";
        }
    } 

그래서, '='을 없애고 비교값 중 하나를 증가시켰고, 가스비를 절감할 수 있었다.

  1. 전투중인 페이지를 구현하는 Fighting.js에서 전투중인 이미지들을 구현할 때 무기의 종류에 따라 이미지 크기들이 다르게 출력되는 문제가 발생하였다. 처음에 시도한 방법은 무기의 고유번호 값에 따라 State값을 각각 이미지로 저장하여 관리하는 방법으로 구현하였다. 그러나, 이미지를 전부 저장하는 과정에서 유저의 전투 이미지와 매칭 유저의 전투 이미지 중 하나가 누락되는 에러가 발생하였습니다.
const Fighting = () => {
  /// 생략...
  const [userImage, setUserImage] = useState();
  const [userSwordImage, setUserSwordImage] = useState();
  const [userBowImage, setUserBowImage] = useState();
  const [userStaffImage, setUserStaffImage] = useState();
  const [userPolearmImage, setUserPolearmImage] = useState();
  const [userWeapon, setUserWeapon] = useState();
  const [matchingImage, setMatchingImage] = useState();
  const [matchingSwordImage, setMatchingSwordImage] = useState();
  const [matchingBowImage, setMatchingBowImage] = useState();
  const [matchingStaffImage, setMatchingStaffImage] = useState();
  const [matchingPolearmImage, setMatchingPolearmImage] = useState();
  /// 생략...


  const fighting = async() => {
    // user 캐릭터 정보
    const fightImage = await metadataAPI.fetchFightImage(chardata.attributes, weapondata.attributes, 'animated');
    setUserImage(fightImage);
    setUserWeapon(userweapon.strength);

    // 매칭 캐릭터 정보
    const Mchardata = matchingdata.matchingChardata
    const Mweapondata = matchingdata.matchingWeapondata
    const MfightImage = await metadataAPI.fetchFightImage(Mchardata.attributes, Mweapondata.attributes, 'animated');
    setMatchingImage(MfightImage);

    if ( account.weaponId === 0 || account.weaponId === 1 || account.weaponId === 2) {
      setUserSwordImage(fightImage);
    } if ( matchingdata.weaponId === 0 || matchingdata.weaponId === 1 || matchingdata.weaponId === 2 ) {
      setMatchingSwordImage(MfightImage);
    } 
	/// 생략...
  }

이 방법은 효율적이지 못하다고 판단하고, State에 이미지를 직접 저장하는 대신, bool값으로 관리 하는 것으로 수정하였다. 이제 State에 다른 이미지가 들어가는 등의 오류는 발생하지 않았고, 무기의 종류가 달라도 이미지의 크기를 동일하게 출력할 수 있었다. 이 문제를 해결한 것에서 그치지 않고, '만약 무기를 확장한다면 조건문이 복잡해질 것이다' 라는 생각을 해봤고, 무기의 고유번호 값을 몫으로 나눈 케이스 별로 구분하여 조건문을 단순화하는 것이 뒤에 있을 확장 작업에도 효율적이라고 생각하여 코드를 수정하였다. 그 결과, 무기를 확장하였을 때, 오류 없이 이미지가 잘 출력 되었다.

const Fighting = () => {
  /// 생략...
  const [userImage, setUserImage] = useState();
  const [userSwordImage, setUserSwordImage] = useState(false);
  const [userBowImage, setUserBowImage] = useState(false);
  const [userStaffImage, setUserStaffImage] = useState(false);
  const [userPolearmImage, setUserPolearmImage] = useState(false);
  const [userWeapon, setUserWeapon] = useState();
  const [matchingImage, setMatchingImage] = useState();
  const [matchingSwordImage, setMatchingSwordImage] = useState(false);
  const [matchingBowImage, setMatchingBowImage] = useState(false);
  const [matchingStaffImage, setMatchingStaffImage] = useState(false);
  const [matchingPolearmImage, setMatchingPolearmImage] = useState(false);
  /// 생략...


  const fighting = async() => {
    // user 캐릭터 정보
    const fightImage = await metadataAPI.fetchFightImage(chardata.attributes, weapondata.attributes, 'animated');
    setUserImage(fightImage);
    setUserWeapon(userweapon.strength);

    // 매칭 캐릭터 정보
    const Mchardata = matchingdata.matchingChardata
    const Mweapondata = matchingdata.matchingWeapondata
    const MfightImage = await metadataAPI.fetchFightImage(Mchardata.attributes, Mweapondata.attributes, 'animated');
    setMatchingImage(MfightImage);


    if ( parseInt(account.weaponId / 100) === 0 ) {
      setUserSwordImage(true);
    } if ( parseInt(matchingdata.weaponId / 100) === 0 ) 
      MatchingPolearmImage(true);
    }
	/// 생략....
  }

  1. 팀원분이 작성한 컨트랙트 중 가스비 한도를 초과하여 truffle에서 배포가 되지 않는 이슈가 발생하였다. script 메타 데이터를 DB와 연동하여 데이터 수량을 늘리는 과정에서 발생한 문제였는데, 테스트 환경이였기에 Ganache 설정에서 가스 한도를 100억으로 변경한 후 임시로 작업을 진행하였다. 추후에 해당 컨트랙트에 해당하는 메타 데이터의 갯수를 제한하여 가스비 에러를 해결하였다.

  2. 팀원분 중 한명에게 컨트랙트의 이해에 대해 도움을 받았던 경우가 있었다. 팀원분은 토큰 스왑 컨트랙트를 구현하는 과정에서 swap, deposite, withdraw는 lastBlockTimeStamp를 기준으로 컨트랙트에 있는 Reserve를 동기화 시키는데 이때, timeElapsed 에서 새로운 블록이 생성될 때 오버플로우가 발생하였다. 에러를 해결하기 위해 검색한 결과 컨트랙트가 컴파일시 언,오버플로우를 체크하는 과정을 unchecked를 사용하면 컨트랙트를 체킹을 하지 않아 오버플로우가 발생하지 않게 가능하다는 것을 알게 되었다고 했고, 가스비 또한 절약할 수 있었다고 하였다. 컨트랙트를 공부하는데 많은 도움이 되었다.

  3. 솔리디티에서 형변환이 되지 않는것을 몰랐을 때, 조건문의 조건이 소수점으로 출력되어 컨트랙트가 배포 되지 않아 시간을 많이 소비하였다.


💬 개발 회고

이번 마지막 프로젝트는 약 30일로 기간이 긴만큼 처음 기획 단계부터 신중하게 진행하였다. 각자 맡은 부분에 대해 각종 자료를 찾아보고 정리하여 최대한 기간안에 기능들을 구현 할 수 있도록 소통을 자주 하였고 Notion을 통해 각자 찾은 자료들을 공유하면서 서로 공부해나갔다. 어느 정도 기획이 끝난 후, 우리는 agile 방식으로 하나씩 구현해 나가기로 했다.

내가 맡은 부분은 전투에 관련된 모든 부분을 담당하는 거였다. 메이플 스토리 에셋을 활용하여 캐릭터 이미지와 무기 이미지 API를 작성하고 전투 Front를 차근차근 구현해나갔다. 프로젝트1, 2를 거치면서 Front를 구현하는 것은 나름 수월하였다. 비록 중간 중간 비동기 에러들이 있었지만, 앞선 프로젝트에서 경험한 것을 토대로 차근차근 해결해나갔다. 그리고 이번 프로젝트에서는 상태 관리를 recoil을 사용하기로 하였다. 공식 문서를 참고 하면서 어렵지 않게 상태 관리를 구축하였다. DB에서 계정 정보를 연동 하는 것 또한 프로젝트2를 통하여 해봤던 작업이라 순탄히 진행되었다.

전투에 관한 front 부분을 어느정도 구현하고 난뒤 우리가 프로젝트3을 구현하면서 피하려고 했던 것 중 하나가 'DB의 사용을 최소화하고 컨트랙트로 구현을 하는 것'이였다. 아무래도 게임 개발이였기에 랜덤과 확률적인 요소가 많이 반영되었고, 이것을 유저들에게 투명하게 보여주기 위함이였다. 그래서 내가 새롭게 작업을 시작한건, 전투의 승/패를 정하는 컨트랙트를 작성하는 것과 전투가 종료 된 후 보상을 랜덤으로 주는 것을 컨트랙트로 구현하는 거였다. 프로젝트에서 컨트랙트를 작성하는 것은 이번이 처음이였기에 막막한 느낌이 있었다. Remix를 이용하여 컨트랙트를 작성하고 끊임없이 컴파일 하면서 에러를 검색하고 찾은 끝에 무사히 컨트랙트들을 구현할 수 있었다.

이번 팀은 특히 나뿐만아니라, 팀원분들이 굉장히 열정적으로 소통을 하면서 작업을 하였다. 밤,낮을 가리지 않고 서로 소통하면서 서로의 진행도를 체크하고 이슈를 공유 하면서 같이 해결해 나가 더 많은 기능들을 구현할 수 있게 되었다. 협업을 할 때 소통이 정말 중요하다는 것을 다시 한번 깨달앗다.

이번 프로젝트3는 내가 블록체인 개발로서 정말 많은 부분에서 성장한 느낌이 드는 프로젝트였다. 이 느낌에서 멈추지 않고 밑에 있을 Future Work에서 구현해 보고 싶은 부분을 나중에 구현해보고자 한다.


📝 Future Work

기능적인 요소

  • 로그인: 다양한 지갑 연결
  • 랜덤 요소: 랜덤 요소에 chainlink를 적용하여 오라클 문제 해결 시도
  • 히스토리: 이용자가 전투 및 거래 History를 볼 수 있는 기능
  • 전투: 하루에 전투를 할 수 잇는 횟수 제한
  • 게임 시스템: 레벨 시스템으로 발전, 레벨에 따라 보상 지급량과 강화서 확률 변화 등
  • 스왑: Factory 패턴을 적용하여 다른 토큰들과의 스오바을 진행할 수 있게 구조 변경, 유동성 풀에 복리(APY) 구조를 넣어 사용자에게 더 높은 이자율을 제공

개선 방안

  • 사용자가 많아질 경우를 대비하여, 5분 단위로 조회. 업데이트 하는 방식 고민, ERC1155의 경우 batch를 사용하여 서버에서 특정 시간에 쌓인 여러 사용자의 요청을 한꺼번에 처리.
  • 가스비 소모를 줄이기 위한 컨트랙트 코드 개선.
  • 폴리곤 테스트넷에 라이브 배포시 로컬과 다르게 레이턴 시로 인해 프론트 엔드 코드 버그 해결

서비스 요소

  • 추후 외부에서 거래 가능 하도록 tokenURI를 설계를 해 놓았고, 이를 활용하여 OpenSea 컬렉션을 등록할 수 있다.

블록체인 커뮤니티

  • 길드 컨텐츠: 길드에 속한 사람들은 전투 시 레어한 확률로 보석 획득, 추후 계속 설계 예정
    ...

Github 코드 주소
https://github.com/codestates/BEB-05-JMT
데모 주소
https://jmt-maplestory.com/login

profile
Blockchain Developer

0개의 댓글