Skip to content

Instantly share code, notes, and snippets.

@pocketberserker
Last active December 25, 2022 13:43
Show Gist options
  • Save pocketberserker/0a8d4c77780de8defd49ec88447205c8 to your computer and use it in GitHub Desktop.
Save pocketberserker/0a8d4c77780de8defd49ec88447205c8 to your computer and use it in GitHub Desktop.

「スクリプト付きアイテム」のためのJavaScript入門(非公式)

本資料では「スクリプト付きアイテム」に採用されているJavaScriptというプログラミング言語について、公式サンプル集で使われている範囲に絞って解説します。

プログラミング経験がある場合は本資料の代わりにJavaScript Primer(あるいは同名の書籍)を取っ掛かりにするか、素晴らしいリファレンスであるMDN Web Docsを参考にしましょう。

本資料に不正確な記述がある場合はぜひご指摘ください(@pocketberserkerにメンションされれば数日内にアクションします)。 できる限り修正対応したいと考えています。

(非公式、と書いたのは会社の業務とは関係ない時間に書いたからです)

環境の準備

動くクラフトアイテムを作ってみようを参考にしつつ、スクリプトエディタやコンソールが使える環境を整えてください。

コンソールの表示の意味

コンソールでは、ユーザーが入力したコードは行頭に >、 入力されたコードを実行した際に取得した結果には行頭に < が表示されます。 また $.log で出力されたメッセージには情報ログを表すアイコン( ℹ️)、スクリプトがエラーになったときのメッセージにはエラーログを表すアイコン(赤色のℹ️)が行頭に表示されます。

本資料では、赤色のℹ️が表せない都合によりエラーログのアイコンを❌で代用しています。

数値と計算

JavaScriptは整数や小数といった数値を扱えます。 コンソールでは入力した数値がそのまま表示されますが、これによりコンソールが数値をちゃんと認識しているとわかります。

> 3
< 3
> -5
< -5
> 1.1
< 1.1

JavaScriptでは数字といくつかの記号を用いた計算は数式として実行されます。 試しに2 + 2とコンソールに入力してエンターキーを押してみると、4という計算結果が表示されるはずです。

> 2 + 2
< 4

JavaScriptにはいくつかの算術演算が用意されており、算術演算に用いられる記号を算術演算子と呼びます。 もっとも簡単な算術演算は四則演算と剰余です。

記法 解説
A + B A 足す B
A - B A 引く B
A * B A 掛ける B
A / B A 割る B
A % B A を B で割った余り

一般的な算数と同様に、ひとつの式の中で複数の四則演算を用いることができます。 このときの計算の優先順序も一般的な概念と同様です(例: */+- より先に計算する)。

> 1 + 2 * 3
< 7

また、計算の優先順序を指定したい場合は()で囲みます。

> (1 + 2) * 3
< 9

文字列

多くのプログラミング言語では、連続した文字の並びを表すデータを文字列と呼びます。 JavaScriptでは'(シングルクォート)あるいは"(ダブルクォート)で囲んだ文字を文字列として扱います。

> 'こんにちは'
< こんにちは
> "Hello Cluster!"
< Hello Cluster!

文字が0個の文字列は空文字列と呼ばれます。

> ''
<

'"で囲わなかった場合はエラーになります。 囲い忘れはプログラミングにありがちなミスなので、エラーが表示されても慌てずに対処しましょう。

> さようなら
❌ ReferenceError: さようなら is not defined

'で囲んだ文字列の中で'を使いたいとき、もしくは"で囲んだ文字列の中で"を使いたいときには\(バックスラッシュ、Windowsでは¥記号が表示されることも)を直前に置く必要があります。 これをエスケープと呼びます。

> 'I\'m cluster user.'
< I'm cluster user.

文字列は他の文字列や数値と+記号で連結することで新たな文字列となります。

> 'Hello' + 'Cluster'
< HelloCluster

> 1 + ' + ' + 1 + ' = ' + 2
< "1 + 1 = 2"

'"に意味的な違いはなく、どちらを使うかは開発者の好みや開発プロジェクトが採用するコーディング規約(プログラムの書き方に関する決まりごと)によって異なります。 本資料では主に'を使用していますが、好みにあわせて"に置き換えても動作に影響はありません。

変数

変数は値に名前をつけ、識別しやすくしたり再利用しやすくするための機能です。 JavaScriptではletキーワードを使うことで変数を定義できます。 変数に値をセットすることを代入と呼びます。

変数が定義されていない場合はエラーとなります。 試しにコンソールにiと入力してください。

> i
❌ ReferenceError :i is not defined

次に変数iを定義し、i10を代入してからiを呼び出してみましょう。 このとき、事前に定義された変数iにセットされた10が表示されます。

> let i = 10
< undefined
> i
< 10

変数名はある程度自由につけられますが、ソースコードを後から読み返すときに理解しやすいような名前をつけるべきです。

定義した変数は、別の変数を定義する際や文字列との連結にも使えます。

> let j = i * 2
< undefined
> i + 'の2倍は' + j + 'です'
< 10の2倍は20です

数値をセットした変数には++(インクリメント)と--(デクリメント)という算術演算子が使えます。 ++は指定した変数にセットされた数値を結果として返しつつ、その数値に1加えた値を変数へセットします。 --は逆に、1減らした値を変数にセットします。

> let i = 0
< undefined
> i++
< 0
> i
< 1
> i--
< 1
> i
< 0

letキーワードで定義した変数には別の値を再代入できます。 再代入する場合にはletは必要ありません。

> i = 5
< 5
> i
< 5

また、演算と代入を省略して記述できる複合代入演算子があります。 例えば左側の変数の値と右側の値を加算した結果を変数に代入する際には+=を用いることもできます。

> i = 1
> i
< 1
> i += 2
> i
< 3

複合代入演算子は数が多いので、他にどういうものがあるかは調べてみてください。

定数

プログラミングでは「毎フレームごとに移動させる距離」といった固定の値に名前を付けたいことがあります。 先ほど説明した変数でも名前を付けられますが、定数という機能を用いると再代入できないこと(値を固定できること)をプログラム上で保証できます。 再代入できないようにすることで意図しない値の変更を検知できるようになります。

JavaScriptではconstキーワードで定数を定義できます。

> const speed = 1.0
< undefined
> const range = 2.0
< undefined

定数には再代入を行えません。 試しに先ほど定義した speed に値を代入してみると、定数に代入できないことがわかります。

> speed = 2.0
❌ TypeError: Assignment to constant variable.

値を変更する必要のない変数は積極的に定数に置き換えましょう (ただし、コンソールでは定数を一度定義すると値を変更できなくなってしまうので、以降のサンプルでは let を用いる場合があります)。

JavaScriptでは処理を実行する1ステップをと呼びます。 文と文は主に;(セミコロン)を使って区切ります。

文;
文;
文;
...

JavaScriptには;がない文にも行末に;を自動挿入する特殊ルールがあります。 しかし、このルールは意図しない挙動を引き起こしやすいため、実際のコードでは文に;をつけることが多いです。

ただし、コンソールにおいて1行ずつ入力してコンソールからの応答を待つ場合は;を省略することがあります。 ここまで;を書かずにいたのはこれが理由です。

また、後ほど登場するif文やfor文の末尾に;を書く必要はありません。

関数定義と呼び出し

プログラムを効率的に書いたり読みやすいものにする道具のひとつに関数があります。 関数は何か値を受け取って、関数内に書かれた処理を実行します(何も受け取らない関数も定義できます)。

例えば、ScriptableItemのソースコード内で使用可能な $.log は渡した数値や文字列、変数をコンソールに表示してくれる関数です(厳密な説明をするとややこしくなるので、ここではまだそういうことにしておいてください)。

> $.log("Hello Cluster!")
ℹ️ Hello Cluster!
< null
> $.log(speed)
ℹ️ 1
< null

コンソールに数字や文字列を直接入力していたときと異なり、インフォメーションの出力として結果が表示されたと思います。 これは、コンソールに値を入力したときに結果が表示されたのは「入力したプログラムによって得られた最後の計算結果を表示する」コンソールの機能であることに対し、$.logは「渡されたその時点の値をコンソールに表示する」という違いがあります。

$.logはScriptableItemであらかじめ用意された関数ですが、関数は自分で定義することもできます。

JavaScriptでは、関数は変数と同様にある程度自由に名前を決められます。 今回は次のような形式で関数を定義することにします(次のコードは実行できません)。

const 関数名 = (引数) => {
  ここに処理を書く
};

まずは単純にHello Cluster!を表示するだけの関数を定義してみます。

> const sayHelloCluster = () => {
    $.log('Hello Cluster!');
  };

関数は定義しただけでは何も起きません。 定義した関数を呼び出すことではじめて、関数内に記述したコードが実行されます。

> sayHelloCluster()
ℹ️ Hello Cluster!
> undefined

関数を呼び出す際に呼び出し側から値を渡せるよう、 引数(ひきすう) を設定することができます。

では、引数の値を用いてHello **!とコンソールに表示するsayHello関数を定義してみましょう。

> const sayHello = (name) => {
    $.log('Hello ' + name + '!');
  };

渡したい値を括弧内に指定して関数を実行します。

> sayHello("Cluster")
ℹ️ Hello Cluster!
> sayHello("World")
ℹ️ Hello World!

何回も似たような処理を行いたい場合や、あるひとまとまりの処理に名前を付けたい場合は関数として定義することを考えてみてください。

真理値

プログラミングで扱うデータは数値や文字列のほかにもさまざまな形式があります。 そのうちのひとつが真理値と呼ばれるデータ形式(データ型)です。

真理値はtrue)とfalse)の2つの値しかないデータ型で、後述する比較演算や論理演算の結果として用いられています。 また、複雑な処理を行うための条件分岐や繰り返し構文を理解するには必須と言ってよい、重要なデータ型になります。

比較演算

例として、ある動いているアイテムの速度itemSpeedが速いか遅いかを判定して出力することを考えてみます。 ここでは10未満を「遅い」、20以上は「速い」、その間は「普通」と定義します。

たとえば「変数itemSpeedの値が10以下(遅い)か」という条件があったとき、この条件の結果は 「条件を満たす(true)」 か 「条件を満たさない(false)」 のいずれかです。 このように値を比較することを比較演算と呼び、その結果は必ず真理値(trueもしくはfalse)となります。

値の比較を行う場合、JavaScriptでは次のような比較演算子を使います。

記法 解説
A < B AがBより小さければ真(AはB未満である)
A <= B AがBより小さいまたは等しければ真(AはB以下である)
A > B AがBより大きければ真(AはBより大きい
A >= B AがBより大きいまたは等しければ真(AはB以上である)
A === B AとBが等しければ真(AとBは等しい
A !== B AとBが異なれば真(AとBは異なる

たとえば、「変数itemSpeedの値は10未満である」という比較をJavaScriptで表現するとitemSpeed < 10となります。

> let itemSpeed = 6.5
> itemSpeed < 10
< true

論理演算

今回考えようとしているスピードチェックで"普通"と判断する条件は「itemSpeed10以上かつ20未満」であることです。 このように、2つの真理値を組み合わせてひとつの真理値を結果とする演算を論理演算と呼び、JavaScriptでは論理演算子を用いて記述します。

記法 意味 解説
A && B 論理積(AND) 左右がどちらも真なら真(AかつB)
A || B 論理和(OR) 左右どちらかが真なら真(AまたはB)
!A 論理否定(NOT) 真理値を反転させる(Aではない

「変数itemSpped10以上かつ20未満」という条件をJavaScriptで表すと次のようなコードになります。

> itemSpeed = 15
> itemSpeed >= 10 && itemSpeed < 20
< true

また、「普通のスピードではない」という条件は「itemSpeed10未満もしくは20以上である」といえます。

> itemSpeed = 8
> itemSpeed < 10 || itemSpeed >= 20
< true

真理値や条件式に!をつけるとtruefalseが反転します。 また、比較演算や論理演算にも通常の四則演算と同様に優先順位があり、()で囲むことにより優先して計算させることが可能です。

次の例では「itemSpeed10未満ではない」という条件をJavaScriptで書いたものになります。

>  !(itemSpeed < 18.5)
< false

比較演算子や論理演算子を使った式を条件式と呼びます。 条件式をうまく使うことにより、「y座標が0以上かつ100以下である」のような複雑な条件をプログラムで表現できるようになります。

条件分岐

たいていのプログラミング言語には、条件式の結果によって異なる処理をさせる条件分岐と呼ばれる機能が存在します。 JavaScriptではif文を用いて条件分岐を表現します(次のコードは実行できません)。

if (条件式) {
  条件式を満たす場合に実行したい処理
}

ifの直後にある()内の条件式がtrueならば、それに続く{}内の処理が行われます。

> itemSpeed = 6;
> if (itemSpeed < 10) {
    $.log('このアイテムは遅いです');
  }
ℹ️ このアイテムは遅いです
> itemSpeed = 15;
< undefined
> if (itemSpeed < 10) {
    $.log('このアイテムは遅いです');
  }
< undefined

条件を満たさない場合(偽の場合)の処理も記述したいときはelse文を使います。 また、else if文を用いてさらに細かい条件分岐をさせることが可能です。

if ~ else文は上から順に処理されていき、条件に合致するものがあれば続く{}の中だけが実行され、他の処理は実行されません(次のコードは実行できません)。

if (条件式1) {
  条件式1を満たす場合に実行したい処理
} else if (条件式2) {
  条件式2を満たす場合に実行したい処理
} else if (条件式3) {
  ...
} else if (条件式N) {
  条件式Nを満たす場合に実行したい処理
} else {
  条件式1~Nを満たさない場合に実行したい処理
}

これで「itemSpeedが10未満は遅い20以上は速い、その間は普通」という複雑な条件分岐を表現できるようになりました。

>  itemSpeed = 30
>  if (itemSpeed < 10) {
     $.log(''このアイテムは遅いです'');
   } else if (itemSpeed >= 20) {
     $.log(''このアイテムは速いです'');
   } else {
     $.log(''このアイテムは普通の速度です');
   }
ℹ️ このアイテムは速いです

配列

プログラミングをしていると、いくつかの同じようなデータを大量に扱いたいことがあります。 JavaScriptでは、複数のデータをまとめて扱うために配列というデータ構造が用意されています。

配列は[]で囲み、それぞれのデータは,区切りで表現します。

let names = ['alice', 'bob', 'charlie']

配列の各データ(要素)にアクセスするには、[]に要素の番号(インデックス)を指定します。 先頭のインデックスが0であることに気をつけてください。

> names[1]
< bob

配列には文字列や数値、変数などさまざまなデータ型を格納できます。

>  let dog = 'プードル'
>  let anything = [10, 'string', dog]
>  anything[2]
< "プードル"

ただし、複数のデータ型をひとつの配列にいれるのは混乱のもとになりやすいため避けるべきです。 複数のデータ型をひとまとめにしたい場合は後述するオブジェクトを利用しましょう。

配列に値を追加したり上書きしたい場合は、インデックスを指定して値を代入します。

> names[2] = 'charlotte'
> names[3] = 'daisy'
> names[2]
< charlotte
> names[3]
< daisy

'charlie''charlotte'に上書きされ、新しく'daisy'が追加されました。 ただ、配列に値を追加する場合は後述するメソッド( 配列、 push で調べてみてください )を用いた書き方を使うほうが多いです。

後から値を設定するために、何も入っていない配列(空配列)を定義することも可能です。

> let numbers = []
> numbers[0] = 0
> numbers[1] = 1
> numbers[2] = 2

繰り返し(ループ)

プログラムが得意な仕事のひとつとして繰り返しがあります。 同じような作業を何回となく繰り返すと人間は疲れたりミスしたり不機嫌になったりしますが、プログラムは文句もいわず間違えず、粛々と作業をこなしてくれます。

プログラムが繰り返し処理を行うことをループと呼びます。 JavaScriptにはいくつかのループ構文がありますが、ここではもっとも基本的なfor文について説明します(次のコードは実行できません)。

for (初期化処理; ループの継続条件; 1ループ終了時の処理) {
    繰り返す処理
}
  • 初期化処理
    • ループの最初に1回だけ実行されます
  • ループの継続条件
    • 条件式が true であれば、直後の {} 内に記述された「繰り返す処理」が実行されます
  • 1ループ終了時の処理
    • 「繰り返す処理」を実行した後に実行されます

初期化処理でループ開始条件を決め、ループの継続条件がtrueである間繰り返す、という流れを想像してください。

> for (let i = 1; i <= 5; i++) {
    $.log(i);
  }
ℹ️ 1
ℹ️ 2
ℹ️ 3
ℹ️ 4
ℹ️ 5
< null

(let i = 1; i <= 5; i++) は次のようなことを表しています。

  • 初期化処理
    • i という変数を定義し、初期値として 1 を代入します
  • ループの継続条件
    • i5 以下の場合、ループを継続します
  • 1ループ終了時の処理
    • i を 1 ずつ増やします

つまり上記のコードは「i1から始まり、1ずつ増加し、i5を超えるまでコンソール表示を繰り返す」ループになります。

ループと配列

ループ(繰り返し処理)は、配列のようなたくさん並んだデータに対して同じ処理を行うときに真価を発揮します。 例として、配列の全データを10倍にして表示するコードを書いてみましょう。

> let numbers = [3, 5, 12, 6, 0];
> let length  = numbers.length;

配列に対して.lengthを記述することで、その配列の要素数を取得できます。 この場合は5length定数として定義しいています。

> for (let i = 0; i < length; i++) {
    $.log(numbers[i] * 10);
  }
ℹ️ 30
ℹ️ 50
ℹ️ 120
ℹ️ 60
ℹ️ 0

オブジェクト

関連する複数のデータをひとまとめにするために利用されるデータ型のひとつがオブジェクトです。 オブジェクトはプロパティの集まりであり、プロパティは名前(あるいはキー)と値のペアから成り立っています(次のコードは実行できません)。

{
  プロパティ名1: 値,
  プロパティ名2: 値,
  ...
  プロパティ名N: 値
}

.(ドット)に続いてプロパティ名を記述すると、オブジェクトに定義されたプロパティに設定されている値を取得できます。 オブジェクトのプロパティには文字列を使ってアクセスすることも可能です。

> let person = {
    name: 'Swanman',
    age: 20
  }
< undefined
> person.name
< Swanman
> person["age"]
< 20

プロパティには関数を設定することもでき、JavaScriptではオブジェクトのプロパティに設定された関数のことをメソッドと呼びます。 メソッドを呼び出す方法は関数とプロパティの呼び出しを合わせた形になります。

> let greeting = {
    hello: () => {
      return 'Hello!';
    }
  }
< undefined
> greeting.hello()
< Hello!

これまで$.logを関数として扱っていましたが、実は$という名前で定義された定数(オブジェクト)のlogメソッドがより正確な表現です。

undefined

前節でオブジェクトのプロパティ呼び出しを紹介しましたが、はたして存在しないプロパティを呼び出したらどうなるでしょう?

> person.icon
< undefined

ということで、存在しないことを表す値undefinedが結果として得られます。

さて、ここからがJavaScript固有の面倒くさい話なのですが、undefinedをif文の条件式に渡した場合は自動的にfalseと判定されます。 さらにundefined!で反転させるとtrueが得られます。 このことを利用して、特定のプロパティに値が設定されていないときに処理を実行させる方法があります。

> if (!person.icon) {
    $.log(person.name + 'さん、iconを設定してください!');
  }
ℹ️ Swanmanさん、iconを設定してください!

null

JavaScriptには意図的に値がないことを表す null が用意されています。 nullundefined は等価な値でない、つまり null === undefinedfalse であることに注意してください。

クラス

クラスはインスタンスと呼ばれるオブジェクトを生成するためのテンプレートです。 テンプレートと書いた通り、クラスを定義しただけではデータは何も生成されません。

正直なんのこっちゃだと思うので、ここでは独自クラスの定義については忘れ、Cluster Creator Kit Scriptが提供しているクラスの使い方をかいつまんで紹介します。

ScriptableItemにはVector2、Vector3、Quaternionという3つのクラスが最初から用意されています。 クラスのインスタンスはnew クラス名()の形式で生成できます。

各クラスにはインスタンスを生成する際に初期値を指定できます。 インスタンス生成時に指定できるものはクラスごとに決まっていて、例えばVector3のxyzに初期値としてそれぞれ0.00.01.0を設定したいならnew Vector3(0.0, 0.0, 1.0)という記述になります。

Vector2、Vector3、Quaternionには色々なメソッドが定義されているので、気になる方はCluster Creator Kit Script Referenceをご確認ください。

コメント

プログラムはこれまでに登場した変数や関数、条件分岐、ループがいくつも集まってできています。

しかし、既存のプログラムからは「どう動くか」は読み取れても「なぜそのような処理を書いたのか」が読み取れないことが多いです。 そういった実装意図をプログラムのそばに記述できるように、プログラミング言語にはコメントという機能が用意されています。 コメントはプログラムを実行する際に無視されるように作られています。

JavaScriptには1行コメント複数行コメントというふたつの書き方が用意されています。 1行コメント//は、//以降の同一行にある文字列をコメントとして扱います。 複数行コメント/* ~ */は、/**/の間に記述されている文字列すべてをコメントとして扱います。

// 1行コメントの例

/*
複数行コメントの例
複数行コメントの例
*/

関数再び

一通りの説明が終わったのでよしサンプルを……と行きたいところですが、関数について少し後回しにしていたことがあります。

無名関数

例として公式サンプル集X軸方向に反復移動するを見てみましょう。

// 基本的な動きをつくることができるスクリプトのサンプルコード集 - X軸方向に反復移動する
const speed = 1.0;
const range = 2.0;
const direction = new Vector3(1.0, 0.0, 0.0).normalize();

$.onUpdate(deltaTime => {
  if (!$.state.initialized) {
    $.state.initialized = true;
    $.state.pivot = $.getPosition();
    $.state.time = 0.0;
  }

  $.state.time += deltaTime;

  $.setPosition($.state.pivot.clone().add(direction.clone().multiplyScalar(Math.sin($.state.time * speed) * range).applyQuaternion($.getRotation())));
});

$.onUpdate(deltaTime => { ... });と関数のようなものがメソッドに渡されていますね。

実は(引数) => {}の形で定義しているものは無名関数と呼ばれているもので、数値や文字列と同様に値として扱えます。 この無名関数を定数として定義したものを本資料では関数と呼んでいたわけです。

サンプルの無名関数に名前をつけ、次のように書くこともできます。

const onUpdateCallback = deltaTime => {
  if (!$.state.initialized) {
    $.state.initialized = true;
    $.state.pivot = $.getPosition();
    $.state.time = 0.0;
  }

  $.state.time += deltaTime;

  $.setPosition($.state.pivot.clone().add(direction.clone().multiplyScalar(Math.sin($.state.time * speed) * range).applyQuaternion($.getRotation())));
}

$.onUpdate(onUpdateCallback);

とはいえ、onUpdateCallbackはスクリプト内で複数箇所で呼び出すわけではありませんし、名前を付けるにしても「updateループ毎に呼び出される関数」以上の情報量を持たせられず、関数もそれほど長くないので名前を付けるほどでもないと考えることもできます。

ちなみに、無名関数の引数が1つの場合は括弧(())を省略できます。

return

別のサンプルインタラクトするとZ軸方向に反復移動するを見てみましょう。

// 基本的な動きをつくることができるスクリプトのサンプルコード集 - インタラクトするとZ軸方向に反復移動する
const speed = 1.0;
const range = 2.0;
const direction = new Vector3(0.0, 0.0, 1.0).normalize();

$.onInteract(() => {
  $.state.enabled = !$.state.enabled;
});

$.onUpdate(deltaTime => {
  if (!$.state.initialized) {
    $.state.initialized = true;
    $.state.pivot = $.getPosition();
    $.state.time = 0.0;
    $.state.enabled = false;
  }

  if (!$.state.enabled) return;

  $.state.time += deltaTime;

  $.setPosition($.state.pivot.clone().add(direction.clone().multiplyScalar(Math.sin($.state.time * speed) * range).applyQuaternion($.getRotation())));
});

if (!$.state.enabled) return;についての説明がなされていなかったので、かいつまんで解説します。

if文は中の処理が1文のみのときは波括弧({})を省略できます。 つまり、先のコードは省略しないで書いたと次の記述と同じ意味になります。

if (!$.state.enabled) {
  return;
}

この括弧を省略するかどうかは人や場合による、といった感じです。 下手に省略してコード変更時にバグを誘発してしまうくらいなら全部省略しないということもありますし、returnくらいわかりやすいものなら省略しても良いのではという話もあります。

returnは呼び出された関数の処理をその場所までで終了するためのキーワードです。 returnに到達した時点で関数の実行が終わるので、その後の行に書かれた関数の処理は実行されません。

先のサンプルの場合だと「!$.state.enabledtrue$.state.enabledfalse)ならこの関数の処理を終える」となります。

サンプルの解説

続編を書いてみました

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment