このライブラリのお話をしています(スター頂けると嬉しいです><)
https://github.com/konbraphat51/HotSoupScript
やりたいこと
まずはHSP3
の布教を一つまみ...
筆者は中学生の時にHSP3
とともに育ちました。
HSP3
は子供心を上手にくすぐったすごい言語です。
子供向け言語というと、「Scratch」のようなブロック型言語が思い浮かびますが、HSP3
はゴリゴリのスクリプトベースのコンパイル式言語です。文法はBASIC
と類似しています。
(解答のみ提出の時代の)情報オリンピックもHSP3
で解いていました。
さあ、これの何がすごいのでしょうか?
何よりも、
自分のスクリプトがグラフィカルに命宿る
ところであると考えています。
これが空文字列のスクリプトを「実行」した結果です。
「実行結果」となるウィンドウが現れます。
HSP3
はWindows APIの操作を十分に簡略化したことが特徴です。(そのため、MacOSは使えない)
中3の僕はC++でウィンドウを表示するだけで1か月かかりました。参考
言ってしまえばこの真っ白なウィンドウに文字書いたり図形書いたり画像貼ったりすることに帰着しますが、そのプリミティブな状態からどんどん自分の思い通りの世界が実現していくことがとても楽しく、中学生の頃は一日中ゲームを作ることだけを考えていました。
燃やされた子供心で、中2で下記のような格闘ゲームを作れてしまいました。(Windows API以外のロジックはフルスクラッチで、2万行のコードとなっていて、かなりイカれた中学生だと自省しております)
ちなみに、さきほどの静的な文字の貼り付けからこんなに動くようになったのはループで「描画→画面リセット→描画→...」を繰り返すことによって実装しています。
描画以外のロジックは全てフルスクラッチというわけですが、中1, 2のプログラミング初心者の能力で出来てしまえた理由は下記があるかと思います。
- 描画APIの簡略化
- プリミティブでややこしくない機能
- シングルスレッドで一方通行で遂行
という3点にあるかと思います。
1.については先ほどの通り。
2.については、「なんでもかんでも代行すると逆にややこしくなる+その仕組みを理解する学習コストがかかる」が問題になることかと思います。
例えばUnity
などのゲームエンジンはオブジェクト管理、物理演算などを代行しますが、その仕様を理解するのが忙しい。分かれば便利なんだけど。特にUnity
はオブジェクト指向(コンポーネント指向)の完成系じゃないですか。あんなの初心者には厳しい。
また、初心者が他人のコードを編集できるわけないので、もし既存のシステムが気に食わなければしんどい。
むしろ、全体のシステムを自分で組み上げた方が、大局観を把握できて小回りも簡単に効いてやりやすいのではないでしょうか。(少なくとも個人的にはそう思いました)
3.にも関わってくる話です。プログラミング初心者にはマルチスレッドはしんどいです。たとえばJavaScript
の入力イベントシステム、Unity
のMonoBehaviour.Update()
呼び出し、など、処理がバラバラだと脳内の管理がしんどい。
プログラムは概念的にも一方通行の方が分かりやすい。
これが先ほど紹介した格闘ゲームのメインルーチン(1フレームごとの処理)ですが、これら全てが一方通行です。(オブジェクト指向を把握した今となってはなかなか違和感があるコードですが、初心者の暴走ということで。)
見知らぬスレッドシステムが自分のコードを拾いに来ることも、変なキー入力イベントが飛んでくるのを待ち構えるのではなく、自分で管理する。これがむしろ、初心者に適していると申します。
(もちろん、開発フローとしてはアジャイル開発的に、メインルーチンに段々機能をスモールステップで盛り込んでいく感じで、最初から大局観があったわけではありません)
まあ、教育全般に言えることですが、記憶消して再現性取らない限り何が有効か分かりませんけどね。
今回の自作ライブラリ
HSP3
は優秀だけど、Windowsしか使えないのは今の時代致命的な問題、みんなに遊んでもらいたい。
そこで活路は最強のマルチプラットフォーム・プラットフォーム「ブラウザ」にあるわけです。
要するに、HTMLのcanvas
の上で、先述した3要件
- 描画APIの簡略化
- プリミティブでややこしくない機能
- シングルスレッドで一方通行で遂行
を満たしたJavaScript
ライブラリを作らんと欲します。
作った
まだまだ関数が完備とは言えませんが、ベースは作れました。
レポジトリと、チュートリアルサイトです。
(このチュートリアルサイトもVue3
でそれなりに頑張ったので、こちらも後日記事にします)
2023/11/14追記
記事書きました
【Vue3】自作ライブラリをその場で実行できる多言語対応チュートリアルサイトを作った
仕様(簡単に)
ハロワ
こんな感じ
async function main() {
DrawText("ageuoiehbuoefa", 100, 100)
}
async function main(){
はこの時点では必要なく、用意しなくとも動作しますが、後々出てくるasync Sleep()
の時の導入がだるくなるので、何も考えずに我慢してもらう感じで。
DrawText()
の中身はcanvasの2Dコンテキスト.fillText()
そのままなのですが、HTMLの指定操作や、そもそものクラス概念(.
直前まで)を隠蔽したいと思い、あえてグローバル関数として用意。
色を変えるのは、HSP3
のsetcolor
と同じノリで。
async function main() {
// デフォルトの色は黒です。
DrawText("Black", 100, 100)
// SetColor関数で色を変更できます。
SetColor("red")
DrawText("Red", 100, 200)
// 次のSetColor()が呼ばれるまで設定は続きます。
DrawText("Red2", 200, 200)
// 描画した後にSetColor()を呼んでも意味はありません。
// プログラムは上から下に流れます。
DrawText("Green?", 100, 300)
SetColor("green")
色を指定する裏グローバル変数があり、それを操作する関数を叩いてから描画系の関数を叩くと変色する、というHSP3
に準じた仕様に。
正直裏グローバル変数みたいな奇妙な概念を取り入れるか迷いましたが、描画関数に毎回色を引数として渡すと煩雑になる点、「プログラムが上から下に流れる」「指定を変えないと前回と同じ色で表示され、融通が利かない」という教育的価値を考え、採用しました。正直どうなんでしょうね。
Sleep関数
ゲームプログラミングをする上では、指定時間止めるという処理が、作品に時間概念を取り入れる上でかなり重要なのですが、意外と標準JavaScript
には用意されていませんでした。
async function Sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
こう実装することで、
async function main() {
// これはすぐに表示されます
DrawText("1", 100, 100)
// プログラムを1000ミリ秒(1秒)止めます
// Sleepの前に"await"を付けるのを忘れないでください
await Sleep(1000)
// これは1秒後に表示されます
DrawText("2", 100, 200)
await Sleep(1000)
SetFont("100px Arial")
SetColor("red")
DrawText("うおおおおおおおお", 100, 300)
}
こんな感じに利用できるように。
while(true)
を無理に使ったBusy WaitだとHTMLイベントがこのスレッドに飛んでこなくなりますので、Promise
を使うことで解決。その代わり、初心者にawait
を書かせることになりますが、こればかりはJavaScript
が悪いということで(泣)
いちおう思考停止でawait
を書こう、という感じで非同期概念そのものを隠蔽しているつもりではありますが。
それとSleep()
の重要な役割として、 while(true)
の無限ループをブラウザをクラッシュさせることなく遂行させられる ことにあります。
先ほどのHSP3
のコードでもお見せしましたが、ゲームシステムは基本的に無限ループの中で、決まったフローを繰り返すことで動作しています。(「フレーム」というゲーム用語は、このループの一回一回を指しているわけです)
しかし、このようなスクリプトを書くと、
while(true){
//フレーム処理
}
描画すらされずに、ブラウザがクラッシュします。
これは描画処理のルーチンを始めブラウザ運行の処理に、システム実行の順番が回ってこないからでして、もちろんHTMLイベントも飛んできません。
これを、こう書くと
while(true){
//フレーム処理
await sleep(1)
}
問題なく動作します。このSleep()
の時間中にブラウザ運行の処理がなされるわけですね。
初心者には「休憩時間」「冷却時間」という風に説明してます。
入力系統
HTMLで入力を扱おうとすると、HTMLイベントを扱わざるを得なくなり、だるい!
ので、隠蔽しました。
async function main() {
// 変数を作り、ついでに初期位置を設定
var x = 100;
var y = 100;
// スピードを設定
var speed = 5;
// 文字の大きさをここで調整する
SetFont("20px Arial")
while (true) {
// 前のフレームの描画を消す
SetColor("white")
DrawRect(0,0,GetCanvasSize()[0],GetCanvasSize()[1])
// 文字を描画
SetColor("black")
DrawText("a", x, y)
// キー入力によって移動
if (GetKey("ArrowLeft")) {
// "ArrowLeft"キーが押されたら、GetKey("ArrowLeft")は`true`になります
// 対応するキーが押されたらこの部分が実行されます
x -= speed;
}
if (GetKey("ArrowRight")) {
x += speed;
}
if (GetKey("ArrowUp")) {
y -= speed;
}
if (GetKey("ArrowDown")) {
y += speed;
}
// 冷却時間
await Sleep(10);
}
}
このGetKey()
関数が、その瞬間にキーが押されているかどうかをboolean
で返しているわけです。
システム側が押されたかどうかを通知してくれるところを、わざわざこちらが毎フレーム確認しにいくという一見馬鹿らしいことをしていますが、この「一方通行」性が初心者に思考の整理を簡単にできている わけです。
ちなみに
大学の課題の物理シミュレーションの実装課題で、教員のBlenderファイルを無視して本ライブラリで「ニュートンのゆりかご」を実装したのがこちらです
(動作ページはREADMEのリンクから)
講座をした
東大GDSCにて、このライブラリを用いた初心者講座を行いました。
参加者は11名で、玄人が4人、別講座でPython入門だけはした方が6名、プログラミング全くの初心者が1名という構成でした。
全員が東大生というわけではないのですが、大半がそうなので、理解の速さなどにおいてはバイアスはないとは全くいえません。そこはご了承を。
5時間ほどで、ハロワから始め、こちらの弾幕ゲームを作ることができました(github pagesに飛ぶ)
初心者向けの講座ですので、かなりスローペースでの進行を心掛けましたが、ここまでいけてしまいました。事後アンケートでも速さについて「ちょっと遅い」が100%でしたので、速さはちょうどいいということですね。
一方的に教えるだけでなく、例えば「これにハイスコア機能を付け足してみてください」と丸投げしても全員できていたので、みんなで書いているコードが全員しっかり理解できていたかと思います。
やっぱり述べてきた3要件が初心者にも取り組みやすいということでしょうかね。
重要なのが、「楽しかった」100%だと思います(お世辞かもしれませんが泣)
大学生だとどうかと思いましたが、やっぱりどんな年でも自分の書いたコードで画面が動くというのは楽しいと思ってもらえるようですね。
僕はプログラミング中毒なので、コマンドプロンプトからの出力でも、なんなら出力しなくとも(?)楽しいのですが、初心者にとっては地味な作業をずっとしているという印象を持ちがち。そこで何かグラフィカルな作品を作るという体験はとてもプログラミングに対してポジティブな感情を呼び寄せることができたのではないでしょうか?
僕自身の思想としては、プログラミングを自己実現のビジネスツールではなく、クリエイティブな楽器として楽しむのが至幸だと考えていますので、皆様にもそう感じてもらえたら嬉しいな。
最後に
いいね頂けると泣いて喜びます><
この記事を読んでいる方は次の記事を読んでいるかもしれません: