はじめに
この記事はまだ自作ニコ生ゲームを作ったこと無い方や、作ってみたい方向けの記事になります。
私の環境がWindowsなのでMacの方は分からないことあったらごめんなさい。でもやることはほぼ変わらないと思います。
完成系の詳細ページです。どんどん改造してください!
事前準備
PCにnode.jsやAkashicEngineを使えるようにする必要があります。
難しいことはほぼ無いです。
この記事に沿って
akashic -V
akashic-sandbox -V
このコマンドでバージョン確認ができたらまず任意の場所に新規フォルダを作りましょう。
デスクトップ推奨しますが、分かりやすければどこでもいいです。
画像変更して投稿までする手順の動画。
ニコニコ動画で公開しています。
興味のある方はぜひ!
https://www.nicovideo.jp/watch/sm44007347
ゲームファイル一式
これはニコ生ゲームという名前の新規フォルダをデスクトップに作ってそこにZipファイルをDLして解凍しているという前提で話を進めていきます。
GigaFile便で期限付きとして配布します。期限切れても気づいたらファイルが残ってる限り更新するので安心してください。
ダウンロードキーは2525です。
https://xgf.nu/VY7Wm
最新更新版はこちら↓
https://xgf.nu/8Ryoa
変更点:
透過背景の場合に透過しすぎて背景と混ざり見にくい場合があるので、クリックで背景色を変えるコードを追加しています。
尚、この記事のコードに関しては最初のまま残しておくのでそのような機能を作ってみたくなった場合は答え合わせみたいな形で見るといいかもです!
必ずしも自分の書き方が正しいわけではないので自分の思う機能ができればそれが正解です!
改造の仕方
このファイルを解凍してひらいていくと
このような配置になっているので、imageフォルダを開き、4枚の画像を変更すればOKです。
ただしファイル名と画像サイズは256x256から変更しないようにお願いします!
画像を変更したあとファイル名はこうなっているか確認してください!
ゲームファイル一式を解凍したあとの手順
cd と入力。 半角スペース1つ入力。 先ほどコピーしたものを貼り付けてエンター
この状態でakashic scan assetと入力してエンター。更新がない場合はこのように表示されます。あるときはバーっと更新内容が表示されます。
xxxxxのところは任意
あとは
akashic export zip --output xxxxx.zip --nicolive
と入力すれば投稿用のZIPファイルが出来上がります。
例:
akashic export zip --output mygame.zip --nicolive
こうした場合mygame.zipというファイルが保存されます。
投稿の仕方
公式の投稿の仕方のページです。
↓ここから投稿ページにも飛べます。
先程の作成したZipファイルをここにアップロードしてサムネイルや
説明文を書いて投稿するボタンを押せば無事ニコ生ゲーム投稿までが完成です!
フォルダづくりからの作業(手順がちょっと増えます)
ここでは ニコ生ゲーム という名前の新規フォルダを作ったとします。
そのフォルダを開くと上部にこのような欄があるので
右クリックしてアドレスのコピーをクリック
おそらく左下にこのようなスペースがあるので cmd または コマンドプロンプトと入力してそれをクリックします。
するとこの画面が出てきます。赤いところはご自身でつけたPCの名前です。
この画面に cd と入力し、半角スペースを1個入、先程のコピーしたものを貼り付けします。
このようになっていればEnterを押します。
押下後はこんな感じ。
ここに
akashic init -t javascript-shin-ichiba-ranking
これをコピペしてそのまま貼り付けてEnter押して下さい。
次のような画面がでたらとりあえずポンポンとEnterを押してください。
最後だけ60にしてますが、あとから変更可能なのでそのままでも大丈夫です。
今回の画像は1280*720の解像度で60fps、というものになります。
このimageフォルダを開いて
256*256サイズの画像を4枚おいてください。
ただし名前は1.png, 2.png, 3.png, 4.pngという感じにしてください。
例:
この画像はこの記事に貼り付けてるので、キャラのとこだけ白塗りして上から新しく自分で絵を描いて保存とかが楽かもしれません。
音声に関してはm4aファイルとoggファイルが必要になります。
名前は
・bgm.ogg
・bgm.m4a
・se1.ogg
・se1.m4a
・se2.ogg
・se2.m4a
という6つが含まれるようにしてください。
プログラム的にはse1が正解音、se2が不正解音になっています。
main.jsというファイルを右クリックしてメモ帳を開きます。
vscodeなどのエディタを持っている場合はそちらで開くほうがわかりやすいと思います。
たくさんの文字列が表示されますが全部消しましょう。
そこにこの記事のコピー用のmain.jsとかいてあるところの
exportsから一番下のexports.main = main;
までをコピーして貼り付けて上書き保存します。
同様にこのgame.jsonも開いて全部消して、コピー用のgame.jsonのところ
をコピーしたものを上書きしてください
その後黒い画面で
akashic scan asset
というコマンドを打って画像ファイルや音声ファイルを更新します。
画像を変更した場合や、音声を変更した場合このコマンドを使わないと
正常に反映されて起動しないので変更した場合はこのコマンドを使いましょう。
使用画像(サイズが 256x256 の4枚)
コピー用のmain.js
exports.main = void 0;
function main(param) {
let scene = new g.Scene({
game: g.game,
// このシーンで利用するアセットのIDを列挙し、シーンに通知します
assetIds: ["1", "2", "3", "4","se1","se2", "bgm"]
});
// 制限時間・・・実際に表示される時間。 ここでは60秒を想定。
let time = 65; //開始まで5秒のカウントがあるため+5の65
/*
ゲームを起動してから終了するまでの時間は
game.json というファイルの "totalTimeLimit": 75 ← この数字を変更する。
このままなら起動してから終了するまでは75秒となる。
現在は起動から終了までの時間が75秒 そこからカウントで5秒 ゲーム時間が60秒
75-65 = 10 となり、時間が0秒になってから約10秒でランキング集計画面に移動する。
※約30秒に変更したい場合(起動してからカウント5秒→ゲーム時間30秒→結果発表まで10秒の流れになります。)
timeを35秒して、game.jsonのtotalTimeLimitを45にする。
*/
// 市場コンテンツのランキングモードでは、g.game.vars.gameState.score の値をスコアとして扱います
g.game.vars.gameState = { score: 0 };
scene.onLoad.add(function () {
// 各アセットオブジェクトを取得します
let image1 = scene.asset.getImageById("1");
let image2 = scene.asset.getImageById("2");
let image3 = scene.asset.getImageById("3");
let image4 = scene.asset.getImageById("4");
/* 改造できそうな部分 ここから*/
/* BGM */
let bgm = scene.asset.getAudioById("bgm").play();
bgm.changeVolume(0.1); //音量調節
/* 背景 */
const backGroundColor = new g.FilledRect({
scene: scene, cssColor: "#01a18600", // ←""のところを"black"に変更すると黒色の背景になります
width: g.game.width, height: g.game.height
});
scene.append(backGroundColor);
/* 画像がランダムで表示されるパネル */
const imageBox = [image1, image2, image3, image4];
function getRandomImage(excludeImage){
let newImage;
do{
newImage = imageBox[Math.floor(g.game.random.generate()*imageBox.length)];
}while(newImage === excludeImage); // 現在の画像と異なる画像が選ばれるまでループ
return newImage;
}
function checkImageMatch(block){
if(isGameStarted && 60 >= time){
if(imagePanel.src === block.src){
add_Score(500);
imagePanel.src = getRandomImage(imagePanel.src); //正解時にランダムに画像を変更
imagePanel.invalidate();
/* 正解のSE */
let ok = scene.asset.getAudioById("se1").play();
ok.changeVolume(0.5);
}else{
/* 不正解のSE */
let ng = scene.asset.getAudioById("se2").play();
ng.changeVolume(0.5);
add_Score(-501);
}
}
}
let imagePanel = new g.Sprite({
scene: scene, src: getRandomImage(null),
width: 256, height: 256,
anchorX: 0.5, anchorY: 0.5,
x: g.game.width/2, y: 200
});
scene.append(imagePanel);
//左から1番目
let one = Image(scene, image1, 100, 420);
scene.append(one);
//2番目
let two = Image(scene, image2, 300+93, 420);
scene.append(two);
//3番目
let three = Image(scene, image3, 500+93*2, 420);
scene.append(three);
//4番目
let four = Image(scene, image4, 700+93*3, 420);
scene.append(four);
one.onPointDown.add(() => checkImageMatch(one));
two.onPointDown.add(() => checkImageMatch(two));
three.onPointDown.add(() => checkImageMatch(three));
four.onPointDown.add(() => checkImageMatch(four));
/* 改造予定部分 ここまで*/
// フォントの生成
let mainFont = Outline("UD デジタル 教科書体 NP-B", "#FFFFFF", "#53a0fe", 5, 40);
let CountFont = Outline("UD デジタル 教科書体 NP-B", "#FFFFFF", "#ff8383", 20, 200);
// スコア表示用のラベル
let scoreLabel = Text("SCORE: 0", mainFont, 38, 40);
scoreLabel.anchorX = 0;
scene.append(scoreLabel);
// 残り時間表示用ラベル
let timeLabel = Text("SCORE: 0", mainFont, 0.9 * g.game.width, 40);
timeLabel.hide();
scene.append(timeLabel);
// 説明ラベル
let tipsLabel = Text(" ← 下から同じ画像をタッチ", mainFont, 1020, 190);
scene.append(tipsLabel);
//ゲーム開始までの秒数
let Count = 5;
// カウントラベル
let CountLabel = Text(`${Count}`, CountFont, g.game.width/2, g.game.height/2);
scene.append(CountLabel);
scene.setInterval(() => {
Count--;
CountLabel.text = `${Count}`;
CountLabel.invalidate();
}, 1000);
// ゲームが開始したかどうかを判定するフラグを追加
let isGameStarted = false;
scene.setTimeout(() => {
timeLabel.show();
CountLabel.hide();
}, 5000);
/* スコアを処理する関数 */
function add_Score(add){
if (time > 0) {
g.game.vars.gameState.score += add;
scoreLabel.text = "SCORE: " + g.game.vars.gameState.score;
scoreLabel.invalidate();
}
}
/* パネル関数・・・(矩形) */
function Rect(scene, cssColor, width, height, x, y){
let block = new g.FilledRect({
scene: scene, cssColor: cssColor,
width: width, height: height,
x: x, y: y, opacity: 0.25,
touchable: true
});
block.onPointDown.add(() => {
block.opacity = 1;
block.modified();
});
block.onPointUp.add(() => {
block.opacity = 0.25;
block.modified();
});
return block;
}
/* パネル関数・・・(画像) 今回のゲームで使っているのはこっち*/
function Image(scene, src, x, y){
let img = new g.Sprite({
scene: scene, src: src,
width: src.width, height: src.height,
x: x, y: y, opacity: 0.3,
scaleX: 0.78125, scaleY: 0.78125,
touchable: true
});
img.onPointDown.add(() => {
img.opacity = 1;
img.modified();
});
img.onPointUp.add(() => {
img.opacity = 0.3;
img.modified();
});
return img;
}
/* テキスト縁取り用フォントの関数 */
function Outline(fontFamily, fontColor, strokeColor, strokeWidth, size){
let huchiFont = new g.DynamicFont({
game: scene.game,
fontFamily: fontFamily, //任意のフォント
fontColor: fontColor, strokeColor: strokeColor, //fontColorが文字の色、strokeColorが縁の色
strokeWidth: strokeWidth, //ここの数字が縁の大きさ
size: size //ここの数字が文字の大きさ
});
return huchiFont;
}
/* テキスト用関数 */
function Text(text, font, x, y){
let message = new g.Label({
scene: scene, text: text,
font: font,
x: x , y: y,
anchorX: 0.5, anchorY: 0.5,
});
return message;
}
let updateHandler = function () {
if (time <= 0) {
scene.onUpdate.remove(updateHandler); // カウントダウンを止めるためにこのイベントハンドラを削除します
}
// カウントダウン処理
time -= 1 / g.game.fps;
timeLabel.text = "TIME: " + Math.ceil(time);
timeLabel.invalidate();
};
scene.onUpdate.add(updateHandler);
// ここまでゲーム内容を記述します
});
g.game.pushScene(scene);
}
exports.main = main;
コピー用のgame.json
{
"width": 1280,
"height": 720,
"fps": 60,
"main": "./script/_bootstrap.js",
"assets": {
"1": {
"type": "image",
"path": "image/1.png",
"width": 256,
"height": 256
},
"2": {
"type": "image",
"path": "image/2.png",
"width": 256,
"height": 256
},
"3": {
"type": "image",
"path": "image/3.png",
"width": 256,
"height": 256
},
"4": {
"type": "image",
"path": "image/4.png",
"width": 256,
"height": 256
},
"_bootstrap": {
"type": "script",
"path": "script/_bootstrap.js",
"global": true
},
"main": {
"type": "script",
"path": "script/main.js",
"global": true
},
"se1": {
"type": "audio",
"path": "audio/se1",
"systemId": "sound",
"duration": 401,
"hint": {
"extensions": [
".m4a",
".ogg"
]
}
},
"se2": {
"type": "audio",
"path": "audio/se2",
"systemId": "sound",
"duration": 201,
"hint": {
"extensions": [
".m4a",
".ogg"
]
}
},
"bgm": {
"type": "audio",
"path": "audio/bgm",
"systemId": "sound",
"duration": 65883,
"hint": {
"extensions": [
".m4a",
".ogg"
]
}
}
},
"environment": {
"sandbox-runtime": "3",
"nicolive": {
"supportedModes": [
"ranking"
],
"preferredSessionParameters": {
"totalTimeLimit": 75
}
}
}
}