このサイトのお話をします:https://konbraphat51.github.io/HSS_examples/
これがレポジトリです:https://github.com/konbraphat51/HSS_examples
やりたいこと
動機
初心者向け自作ライブラリを作った!(Qiita記事へ飛ぶ) 布教したい!(GitHubレポジトリへ飛ぶ) チュートリアルサイトを作ろう! という経緯です。
ちなみに上記記事が2023/11/14トレンド1位でとても嬉しいです。みなさまありがとうございます。
どんな自作ライブラリか簡単に
https://qiita.com/konbraphat51/items/b138683db352afd77714 でも十分説明しましたが、HTMLの`canvas`要素に対する描画処理APIを単純化した初心者向けゲームプログラミングJavaScriptライブラリです。
すなわち、ブラウザで動作することを想定しております。
要件定義
まず、前提として
- 布教
- プログラミング初心者の手引き
- 多言語対応
をしたいというお気持ちがあります。
-
自分のライブラリを布教するには、 どのようなコードで何が実現できるのか ただちに提示できると訴求性がある。
-
そして、プログラミング初心者を手引きするには、 コードがコピペでき、コメントによる説明が豊富 である必要があります。
-
ついでに、名だたる列強国と戦うためには、言語切替 の機能をつけたいと申します。
1と2からは直ちに 「ちゃんと説明されたコードとその実行結果を同時に見せる」 という結論が得られますが、3により コードコメンテーションも対応言語に変化させる必要 があります。
言語ごとにjsファイルを書く?嫌だなあ。 綺麗に、変更対応がやりやすいように まとめます。
作りました
紹介
作りました。
https://konbraphat51.github.io/HSS_examples/
項目はまだ未完なので、「動く」→「キー入力」のコネクションが飛び過ぎ(2023/11/14現在)なのはご容赦ください。
まず簡単な紹介。
ちゃんと言語切り替えできる。
言語切り替えできる。
選ぶと、自動スクロールで対応するコードまで遷移。
コードも言語切り替えできる!!!
再生ボタンを押すと、対応した処理がcanvasになされる。
フッター。
実装
CDNでお届けしております
npm_modules
アンチなので、必要なもの全てをインストールしておくVue CLI
ではなく、ページを開いた際に、その都度どこか別のサーバーから必要物をダウンロードしてくるVue CDN
で実装。
CDN Vueで、単一ファイルコンポーネント構成を使い、他言語対応する方法の記事はこちら
構成
src
フォルダに本アプリに必要な物品を。
src
は"Source Cord"の略なので、原義通りにきっちりソースコードのみを入れる人もいれば、画像でもなんでも必要なデータすべて入れ込む人もいますよね。
Vue CLI
では後者が主流なので、同じように。
index.html
GitHub Pagesに対応させるため、ルートディレクトリに配置。
先ほど紹介した記事のテンプレートとほぼほぼ同じで、app
のdiv
タグに、App
コンポーネントをマウントさせています。
(例の自作ライブラリを呼び出しているところだけ追加しています)
Examplesフォルダ
Examplesフォルダには、各項目のフォルダが入っていて、その中に
- script.js: 例示するコード
- description.json: 紹介文など
が入っています。
description.json
各言語の説明情報をjsonで記述。これはi18n
の記述と同じですね。
{
"en": {
"title": "Movement",
"option_description": "Give your character a movement"
},
"ja": {
"title": "動く",
"option_description": "文字を動かそう!"
}
}
script.js
初心者を手引きする感じのコメントとともに、動作させるコードを書きます。
async function main() {
//en: This is the combination of "Infinite loop" and "Drawing a rectangle"
//en: Try think how you can move a letter without leave a trace
//en: It will be very satisfying if you come up with a solution
//ja: 「無限ループ」と「四角形を描く」のコラボレーションです
//ja: 先に、跡を残さずに文字を動かす方法を考えてみてください
//ja: 自分で分かったら気持ちいいですよ
//en: make variable + set initial position here
//ja: 変数を作り、ついでに初期位置を設定
var x = 100;
var y = 100;
//en: set speed here
//ja: スピードを設定
var speed = 5;
//en: Adjust character size here
//ja: 文字の大きさをここで調整する
SetFont("20px Arial")
while (true) {
//en: clear all drawings of the previous frame
//ja: 前のフレームの描画を消す
SetColor("white")
DrawRect(0,0,GetCanvasSize()[0],GetCanvasSize()[1])
//en: move by key input
//ja: キー入力によって移動
SetColor("black")
DrawText("a", x, y)
//en: move
//en: Because the `x` is altered, the position moves
//ja: 移動する
//ja: `x`が変化するので、位置が移動する
x += speed;
//en: cool time
//ja: 冷却時間
await Sleep(10);
}
}
実行させるコンポーネントがこのmain()
を呼び起こす、という感じです。
また、コメントに言語コード+:
をつけ、対応した言語のコメントを記入。
表示時に、対応言語のみ残っていますね。こうすることで、言語ごとに.js
ファイルを作らず、まとめて多言語対応コメントをつけることができました。
.vue
ファイル
基本記述
最初の説明カードのコンポーネントの例です。
<template>
<div class="warning">
<p>
<img src="src\images\description\mark_exclamation.png"
width="15"
height="15">
{{ $t("description.warning") }}
</p>
</div>
<div id="description">
<h2>{{ $t("description.what_is") }}</h2>
<p> {{ $t("description.what_is_description") }} </p>
</div>
</template>
<script>
export default Vue.defineComponent({
name: 'Description',
setup() {
//set up i18n
const { t } = VueI18n.useI18n()
return { t }
}
})
</script>
<i18n>
{
"en": {
"description": {
"what_is": "What is HotSoupScript?",
"what_is_description": "This library enables programming beginers to make an artistic (especially game) programming easily.",
"warning": "If the \"Hello World\" script doesn't run (canvas shows nothing), refresh the page."
}
},
"ja": {
"description": {
"what_is": "HotSoupScriptとは?",
"what_is_description": "このライブラリは、プログラミング初心者が簡単にグラフィカルな(特にゲーム)プログラミングを行えるようにするものです。",
"warning": "もし「Hello World」スクリプトが動かない(キャンバスに何も表示されない)場合は、ページをリロードしてください。"
}
}
}
</i18n>
<style>
#description {
margin: 10px 20px 10px 20px;
background-color: #faffdb;
border-radius: 10px;
padding: 10px;
border: 1px solid #000000;
}
.warning {
margin: 10px 20px 10px 20px;
background-color: #ffdbdb;
border-radius: 10px;
padding: 10px;
border: 1px solid #000000;
}
</style>
まあ基本的なVue
の単一ファイルコンポーネントですよね。
一番単純なものを選んでいるわけですが。
項目リスト
一番根本のコンポーネントであるApp
に、存在する項目IDをメタ書き(別JSONに書き出してもよかったけど、App.vue
に書き出すのとあまり違いが分からなかったため)
data() {
return {
examples: [
"HelloWorld",
"ChangeColor",
"ChangeFont",
"Calculation",
"Variables",
"Sleep",
"For",
"WhileTrue",
"Rect",
"Movement",
"MoveByKey",
"BulletGame"
],
selected_example: "HelloWorld",
example_descriptions: {}
}
},
Examplesデータの取得
Examples
フォルダの各項目の説明文を取得するにはfetch
を利用。これも根本のApp
にやらせています。神コンポーネント臭くなりましたね。
getExampleData() {
//read description files
for (let example of this.examples) {
const path = "src/Examples/" + example + "/description.json"
fetch(path)
.then(response => response.json())
.then(data => {
this.example_descriptions[example] = data
})
}
},
子コンポーネントにデータを配布するわけですが、App
の時点で選択言語のデータのみ抽出。computed
に指定することで、言語切り替え時に自動で更新されるわけです。
computed: {
example_descriptions_locale() {
let locale = i18n.global.locale.value
let descriptions = {}
for (let example in this.example_descriptions) {
descriptions[example] = this.example_descriptions[example][locale]
}
return descriptions
}
}
スクリプトの表示
先ほどお見せしたjavascript
ファイルを、対応言語のみ、en:
などのシンボルを消して表示させます
if (line.match("//")) {
//commentation included in the line
let another_language = false
let line_splited = line.split("//")
let line_edited = line_splited[0]
for (let cnt = 1; cnt < line_splited.length; cnt++) {
language_symbols.forEach(symbol=>{
if (line_splited[cnt].match(symbol)) {
if (i18n.global.locale.value == language_symbols_dictio[symbol]) {
//same language
line_splited[cnt] = line_splited[cnt].replace(symbol, "")
} else {
//another language
another_language = true
}
}
})
line_edited += "//" + line_splited[cnt]
}
//続く
スクリプトを走らせる
まずは、読み込みです。
実は、実行に当たって一点ソースコードを改変する必要があります。というのは、サブルーチンを名指しで止める手段がないので、停止ボタンで停止できるように、停止フラグの受信機を作る必要があります。
そこで、自作ライブラリにwhile(true)
をするときはSleep()
を使うというお約束がありますが、Sleep()
の直前に受信機を作ります。
computed: {
script_edited(){
let output = this.script
//define stop flag
output = "var __stop = false\n" + output
let returner = "if (__stop) { return }\n"
//set return under all await() functions
let lines = output.split(/\r\n|\r|\n/)
let cnt = 0
while (cnt < lines.length) {
if (lines[cnt].match("Sleep")) {
//await() function
//set return under the await() function
lines.splice(cnt + 1, 0, returner)
cnt += 2
} else {
//not await() function
cnt += 1
}
}
output = lines.join("\n")
return output
}
そして再生。
再生ボタンのコンポーネントに実行処理を背負わせています。
ゲームプログラミングだと、こんな辺境のUIが重大な処理を背負っていると拡張性の観点から怒られますが、Webフロントエンドのコンポーネント指向の場合はどうなんでしょうね?多分ダメだと思う。
//start
//stop flag
if (typeof(__stop) == "boolean") {
__stop = false
}
//reset HSS condition
SetColor("black")
SetFont("10px sans-serif")
//走らせる
StartAsync(main)
使用感調査
内容はまだ制作中なので置いておいて。
とりあえず個別でプログラミングを教えている方に、ちょっとやらせてみれば「やりやすーい!」とのこと。よかったです(n=1)
最後に
いいね頂ければ泣きながら喜びます。(Vue
記事はあまり見向きされない傾向があります泣)