Skip to main content

Command Palette

Search for a command to run...

Struct(構造体)と Resource(リソース)の全体像

Updated
6 min readView as Markdown
T
Founder & Architect of Blsqui. Building the cloud-native infrastructure layer that transforms standard video games into instant Web3 eSports platforms. Passionate about democratizing global revenue networks for the next generation of independent, AI-driven creators. Creating plug-and-play, no-code backend frameworks for Unity and Godot that integrate frictionless stablecoin microtransactions without client-side execution overhead.

Struct の宣言方法

  access(all) struct Info {

    access(contract) var orderQueue: [Order]

    /* setter */
    access(contract) fun setOrder(execTime: UFix64) {
      var order = Order(execTime: execTime)
      self.orderQueue.append(order)
    }

    init() {
      self.orderQueue = []
    }
  }

Struct の初期化方法

access(all) contract RideShare {
    access(self) let info: Info

    access(all) fun getInfo(): Info {
        return self.info
    }

    init() {
        self.info = Info()
    }
}

ここではContract型のinit内で初期化していますが、一つ上のコードで書いたようにStruct型のメソッド内で初期化することもできます。

Struct型の中で別のStruct型を持つことができます。


Resourceの宣言方法

  access(all) resource Admin {

    access(all) var name: String

    init(name: String) {
      self.data = name
    }
  }

Resourceの初期化方法

リソースはStructと違って必ずアカウントのストレージに保存しなければならないので、もしスマートコントラクトのデプロイ時に初期化を行いたい場合は以下のようにします。

access(all) contract RideShare {

       :

    init() {
        self.account.storage.save( <- create Admin("Admin"), to: /storage/RideShareAdmin)
    }
}

create キーワードを使用します。

createキーワードは必ずスマートコントラクト内に書く必要があります。(createキーワードはトランザクションコード内で使用出来ない

    access(all) fun createWarriorResource(payment: @FlowToken.Vault): @MMORPG.Warrior {
        pre {
            payment.balance == 5.0: "Payment must be 5.0 FLOW."
        }

        let warrior <- create Warrior()
        MMORPG.FlowTokenVault.borrow()!.deposit(from: <- payment) 

        return <- warrior
    }

支払いに応じてリソースを作成しています。このメソッドをトランザクション内から実行します。

: @MMORPG.Warrior 

となっている通り、リソースをトランザクションコードに返す場合、返却する型の前には @ をつけます。


トランザクション内でストレージにリソースを保存

/* Create a Warrior resource */
signer.storage
    .save(<- MMORPG.createWarriorResource(payment: <- payment),
            to: /storage/MMORPGWarriorResource)

トランザクション内からcreateWarriorResourceメソッドを呼んでいます。

リソースはContract内でしか生成できないので、管理しやすく、管理番号をつけることが容易になります。


コントラクト内で管理番号やCapabilityを持てる

access(all) contract RideShare {

    access(self) var totalDrivers: UInt // Driver ID
    access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>

    init() {
        self.totalDrivers = 0
        self.FlowTokenVault = self.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
    }
}

このようにContractStructResourceの中に持てる変数or定数はフィールド(Field)と呼ばれ、必ずinit内で初期化します。フィールドが一つもなければinitは不要です。

ResourceStructは初期化時に引数を渡せます。Contractの初期化時に引数を渡すにはトランザクションコードのsigner.contracts.addに引数を渡します。


スマートコントラクトのデプロイ時に引数を渡す

transaction(contractName: String, code: String, title: String) {
    prepare(signer: auth(AddContract) &Account) {
        
        signer.contracts.add(
            name: contractName,
            code: code.utf8,
            gameTitle: title // 引数1
        )
    }
}
access(all) contract MyGameItem {
    access(all) var gameTitle: String

    init(gameTitle: String) {
        self.gameTitle = gameTitle
    }
}

まとめ

- 特徴 -

構造体:

  • 静的変数のように使える。但し、厳格に型を定める必要がある。

  • 宣言時は必ずaccess(all)であり、構造体の型は公開される。

  • init以外での構造体の値書き換えにはaccess(contract)より小さいアクセス修飾子がついたSetter関数が必要

リソース:

  • クラスオブジェクトに近い

  • ポインタの概念を持つ (borrowで拝借することでストレージから移動せずともフィールドにアクセスできる)

  • 宣言時は必ずaccess(all)でありクラスの型は公開される

  • create演算子でオブジェクトを生成し、オブジェクトはストレージに保存される

  • cpu消費を避ける為、基本的にはポインタを通じてresourceのフィールド/メソッドにアクセスする(移動はトランザクションコストが余計にかかる為)

  • ストレージ間を移動できる。所有できる。

両者に共通:

  • インタフェース(抽象型)を継承できる

  • 任意の数のインタフェースを継承することができる

  • Swift言語に近いがJava言語にも文法的に似ている(インタフェースを型にするインターセクション<&{FungibleToken.Receiver}> はJava言語と同じ書き方で、このように<&{}>で囲む。そのためJavaエンジニアにとっても親しみやすい文法となっている)


- アクセス方法/保存場所 -

構造体:

  • スマートコントラクトのaccess(all)メソッドが入り口となって値を読み書きできる

  • スマートコントラクトがデプロイされたアカウントのストレージ内に保存される

リソース:

  • スマートコントラクトのaccess(all)メソッドを介してリソースを生成でき、アカウントのストレージ内に保存される

  • リソースのメソッドを通じてスマートコントラクトの構造体の値も読み書きできる(逆は構造的に不可能)

  • リソースが他のリソースを保持する事も可能である


スマートコントラクトの一例

import "FlowToken"
import "FungibleToken"

access(all) contract MMORPG8 {

  access(self) let FlowTokenVault: Capability<&{FungibleToken.Receiver}>
  access(self) let info: Info
  access(self) var totalGamePlayers: UInt // Player ID

  access(all) entitlement ThiefAbility1
  access(all) entitlement ThiefAbility2
  access(all) entitlement WarriorAbility1
  access(all) entitlement WarriorAbility2

  access(all) event ESportsGamerInitiated(battleId: Int, player1Address: Address, player2Address: Address)

  /* 構造体 */
  access(all) struct Info {

    /* fields */
    access(contract) var battleQueue: [GameBattle]
    access(contract) var basicAbility: {String: String}     // リソース別の基本アビリティ
    access(contract) var basicAttackInfo: {String: [UInt8]} // システム側から通常攻撃トランザクションを行うための情報

    /* setter  <Return: Battle ID> */
    access(contract) fun newGameBattle(
      player1Address: Address,
      player2Address: Address,
      lifeOfPlayer1: UInt8,
      lifeOfPlayer2: UInt8,
      reward: UFix64
    ): Int {
      var battle = GameBattle(
        player1Address: player1Address,
        player2Address: player2Address,
        lifeOfPlayer1: lifeOfPlayer1,
        lifeOfPlayer2: lifeOfPlayer2,
        reward: reward
      )
      self.battleQueue.append(battle)
      return self.battleQueue.length - 1
    }

    /* setter */
    access(contract) fun updateGameBattle(
      battleId: Int,
      target: String?,
      lifeChange: UInt8,
      isDamage: Bool
    ) {
      if (target != nil) {
        self.battleQueue[battleId]!.setPlayerLife(target: target!, lifeChange: lifeChange, isDamage: isDamage)
      } else {
        self.battleQueue[battleId]!.setEnemyLife(damage: lifeChange) // 敵への攻撃は全体攻撃しかない..
      }
    }

    /* init */
    init() {
      self.battleQueue = []
      self.basicAbility = {
       "Warrior-BasicAttack": "Rage Attack",
       "Warrior-ShareableAbility": "LargeRecoveryShield",
       "Thief-BasicAttack": "Whip Attack",
       "Thief-ShareableAbility": "PoisonMaking",
       "Enemy-BasicAttack": "A Crushing Blow"
      }
      self.basicAttackInfo = {
        "Warrior": [1, 3], // [GroupAttack?, damageVolume] (Rage Attack)
        "Thief": [1, 1], // [GroupAttack?, damageVolume] (Whip Attack
        "Enemy": [0, 4] // [GroupAttack?, damageVolume] (A Crushing Blow)
      }
    }
  }

  /* Info内の構造体 */
  access(all) struct GameBattle {

    /* fields */
    access(all) let execTime: UFix64
    access(all) let player1Address: Address
    access(all) let player2Address: Address
    access(all) var lifeOfPlayer1: UInt8
    access(all) var lifeOfPlayer2: UInt8
    access(all) var enemy1Life: UInt8
    access(all) var enemy2Life: UInt8
    access(all) let reward: UFix64

    /* setter(ダメージまたは回復) */
    access(contract) fun setPlayerLife(target: String, lifeChange: UInt8, isDamage: Bool) {
      if (target == "TeamPlayer1") {
        if (isDamage) {
          self.lifeOfPlayer1 = self.lifeOfPlayer1 < lifeChange ? 0 : self.lifeOfPlayer1 - lifeChange
        } else {
          self.lifeOfPlayer1 = self.lifeOfPlayer1 + lifeChange
        }
      } else if (target == "TeamPlayer2") {
        if (isDamage) {
          self.lifeOfPlayer2 = self.lifeOfPlayer2 < lifeChange ? 0 : self.lifeOfPlayer2 - lifeChange
        } else {
          self.lifeOfPlayer2 = self.lifeOfPlayer2 + lifeChange
        }
      }
    }
    /* setter(敵への全体攻撃) */
    access(contract) fun setEnemyLife(damage: UInt8) {
      self.enemy1Life = self.enemy1Life < damage ? 0 : self.enemy1Life - damage
      self.enemy2Life = self.enemy2Life < damage ? 0 : self.enemy2Life - damage
    }

    /* init */
    init(player1Address: Address, player2Address: Address, lifeOfPlayer1: UInt8, lifeOfPlayer2: UInt8, reward: UFix64) {
      let modulo: UInt8 = 3
      self.execTime = getCurrentBlock().timestamp
      self.player1Address = player1Address
      self.player2Address = player2Address
      self.lifeOfPlayer1 = lifeOfPlayer1
      self.lifeOfPlayer2 = lifeOfPlayer2

      // 両方戦士タイプなら悪魔を登場させる
      if (lifeOfPlayer1 > 5 && lifeOfPlayer2 > 5) {
        self.enemy2Life = 9 + revertibleRandom(modulo: modulo) // 9 ~ 11の整数
      } else {
        self.enemy2Life = 5 + revertibleRandom(modulo: modulo) // 5 ~ 7の整数
      }
      self.enemy1Life = 8 + revertibleRandom(modulo: modulo) // 8 ~ 10の整数
      self.reward = reward
    }
  }

  // GET
  access(all) fun getInfo(): Info {
    return self.info
  }

  // PUT
  access(all) fun newGameBattle(
    payment: @FlowToken.Vault,
    player1Address: Address,
    player2Address: Address,
    lifeOfPlayer1: UInt8,
    lifeOfPlayer2: UInt8
  ) {
    let battleId = self.info.newGameBattle(
      player1Address: player1Address,
      player2Address: player2Address,
      lifeOfPlayer1: lifeOfPlayer1,
      lifeOfPlayer2: lifeOfPlayer2,
      reward: payment.balance * 2.0)

    emit ESportsGamerInitiated(battleId: battleId, player1Address: player1Address, player2Address: player2Address)

    // Game Feeをシステムアカウントに保存
    self.FlowTokenVault.borrow()!.deposit(from: <- payment)
  }

  access(all) resource Thief {
    access(all) let id: UInt
    access(all) let maxLife: UInt8

    access(ThiefAbility1) fun PoisonMaking(battleId: Int) {
      MMORPG8.info.updateGameBattle(battleId: battleId, target: nil, lifeChange: 6, isDamage: true)
    }
    access(ThiefAbility2) fun WhipAttack(battleId: Int) {
      MMORPG8.info.updateGameBattle(battleId: battleId, target: nil, lifeChange: 1, isDamage: true)
    }
    
    init() {
      MMORPG8.totalGamePlayers = MMORPG8.totalGamePlayers + 1
      self.id = MMORPG8.totalGamePlayers
      self.maxLife = 5
    }
  }

  access(all) resource Warrior {
    access(all) let id: UInt
    access(all) let maxLife: UInt8

    access(WarriorAbility1) fun LargeRecoveryShield(battleId: Int, target: String) {
      MMORPG8.info.updateGameBattle(battleId: battleId, target: target, lifeChange: 4, isDamage: false)
    }
    access(WarriorAbility2) fun RageAttack(battleId: Int) {
      MMORPG8.info.updateGameBattle(battleId: battleId, target: nil, lifeChange: 3, isDamage: true)
    }

    init() {
      MMORPG8.totalGamePlayers = MMORPG8.totalGamePlayers + 1
      self.id = MMORPG8.totalGamePlayers
      self.maxLife = 8
    }
  }

  access(all) resource Admin {
    access(all) fun payRewards(recipient1: Address, recipient2: Address, reward: UFix64) {
      // Pay the reward.
      let reward1 <- MMORPG8.account
        .storage
        .borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!
        .withdraw(amount: reward) as! @FlowToken.Vault

      getAccount(recipient1)
        .capabilities
        .get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
        .borrow()!.deposit(from: <- reward1)

      let reward2 <- MMORPG8.account
        .storage
        .borrow<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>(from: /storage/flowTokenVault)!
        .withdraw(amount: reward) as! @FlowToken.Vault

      getAccount(recipient2)
        .capabilities
        .get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
        .borrow()!.deposit(from: <- reward2)
    }

    access(all) fun basicAttack(battleId: Int, target: String?, resourceName: String) {
      MMORPG8.info.updateGameBattle(battleId: battleId, target: target, lifeChange: MMORPG8.info.basicAttackInfo[resourceName]![1], isDamage: true)
    }
  }

  access(all) fun createWarriorResource(payment: @FlowToken.Vault): @MMORPG8.Warrior {
    pre {
      payment.balance == 5.0: "Cost of resource must be 5.0FLOW coin."
    }
    let _resource <- create Warrior() // resourceは予約語なので_をつけてます

    MMORPG8.FlowTokenVault.borrow()!.deposit(from: <- payment) // 代金徴収
    return <- _resource
  }

  access(all) fun createThiefResource(payment: @FlowToken.Vault): @MMORPG8.Thief {
    pre {
      payment.balance == 5.0: "Cost of resource must be 5.0FLOW coin."
    }
    let _resource <- create Thief()

    MMORPG8.FlowTokenVault.borrow()!.deposit(from: <- payment) // 代金徴収
    return <- _resource
  }

  init() {
    self.info = Info()
    self.FlowTokenVault = self.account.capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
    self.totalGamePlayers = 0
    self.account.storage.save( <- create Admin(), to: /storage/MMORPG8Admin)
  }
}

スマートコントラクトは内部のフィールドに変更があると更新ができません。

その為、フィールドを追加したい場合、コントラクト名を変更するか、デプロイするアカウントを変更する必要があります。コントラクトは削除できないため、"*削除&再デプロイ"*はできません。

3 views