みなさんレトロなゲームは好きですか? たとえばドット絵のゲーム。その中でもRPGなどいかがでしょうか。
私にとって、ドットのRPGゲームとは現在の自分を構成した要素のようなものです。
そもそもプログラマーを目指したのもそれがきっかけであり、私の人生に多大な影響を及ぼしました。
特にドラクエやFF、ロマサガは一世を風靡したゲームと言っても過言ではありません。私もこれらのタイトルにどハマりしてしまい、一日中プレイしていた時期がありますm(__)m
それほどまでに夢中だったのです(^^)。FFやロマサガに至ってはシリーズ全てをほぼ堪能しました。とにかく、当時のスクウェアは神がかっていましたね(今は合併して、みなさんご存知の株式会社スクウェア・エニックス。もちろん今も素晴らしいです)。
実際、今もなお愛され続けていることを証明するかのように、そのどれもがピクセルリマスター版として発売されています。
もちろん、2Dドットは引き継がれていますし、しかもドット絵の特徴を壊すことなく、美しく鮮やかにリメイクされました。私にとっては感無量です。
そこで昔ながらのドット絵で動く、レトロRPG的なブラウザゲームをjQuery(JavaScript)でつくってみようと思います。
とは言ったものの、私にとってはとてつもないボリュームなので、まずは少しずつですが……。
ゆくゆくはモンスターとの戦闘、経験値の取得、それによるレベルアップ。そしてラスボス。そんな感じでつくってみようと思います。
まずは村?のようなフィールドを歩かせ、村人と会話する部分だけをつくってみました。
画像に関連する素材は『ぴぽや倉庫(ぴぽや https://pipoya.net/)』様から利用させていただいております。
音素材は『OtoLogic(https://otologic.jp)』様から利用させていただいております。
↓とりあえず、それっぽいのが完成しました2023/6/19
HTMLとCSS
HTMLとCSSの全文です。
<html lang="ja">
<head>
<meta charset="utf-8">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<style>
/* プレイ画面の親CSS */
.game_container{
position: relative;
align-items: center;
height: 200px;
width: 100%;
margin-bottom: 0;
background-image: url(https://likehuman.info/wp-content/uploads/2022/11/grass.png);
background-repeat: repeat;
}
/* メインキャラ(勇者) のスプライト */
.main_character{
position: absolute;
top: 32px;
left: 0px;
clip: rect(32px, 32px, 64px, 0px);
}
/* wordpress(テーマの方?)では、
max-widthが100パーセントに設定される。
描画に支障をきたすので解除 */
img{
max-width: unset;
}
/* コントローラー各種の親CSS */
.control_container{
display: flex;
margin-top: 1vh;
align-items: left;
width: 100%;
position: relative;
}
/* 十字キー */
.cross_key_container{
margin-right: auto;
margin-left: auto;
display: grid;
gap: 10%;
width: 50%;
grid-template-columns: 30% 30% 30%;
grid-template-rows: 160% 160% 160%;
}
.up_btn{
grid-row: 1/2;
grid-column: 2/3;
}
.left_btn{
grid-row: 2/3;
grid-column: 1/2;
}
.right_btn{
grid-row: 2/3;
grid-column: 3/4;
}
.down_btn{
grid-row: 3/4;
grid-column: 2/3;
}
.up_btn,.left_btn,.right_btn,.down_btn{
border: none;
border-bottom: solid 5px #a9a9a9;
border-radius: 8px;
}
.up_btn:active,.left_btn:active,.right_btn:active,.down_btn:active{
border-bottom: solid 2px #a9a9a9;
}
/* BGM、A、Bボタン */
.a_and_b_btn{
display: grid;
margin-right: auto;
margin-left: 20%;
font-size: 0.5em;
row-gap: 50%;
width: 50%;
grid-template-columns: 30% 30% 30%;
grid-template-rows: 160% 160% 160%;
}
.bgm_btn{
grid-row: 1/2;
grid-column: 2/3;
background-color: aliceblue;
}
.a_btn{
grid-row: 2/3;
grid-column: 1/2;
background-color: pink;
}
.b_btn{
grid-row: 2/3;
grid-column: 3/4;
background-color: lightskyblue;
}
.bgm_btn,.a_btn,.b_btn{
border-radius: 80%;
border: none;
border-bottom: solid 5px #a9a9a9;
font-size: 1em;
}
.bgm_btn:active,.a_btn:active,.b_btn:active{
border-bottom: solid 2px #a9a9a9;
}
/* 会話画面の親CSS */
.conversation_container{
margin: 0 auto;
}
/* 会話画面 */
.conversation{
width: 0;
height: 0;
background: black;
display: none;
left: 50%;
border: 5px solid #249dec;
position: relative;
font-size: 1em;
color: white;
margin-top: 0;
}
/* 空白領域 */
.blank{
height: 100px;
}
/* wordpressで正常に表示されない、または動かない時にチェックする項目
・クラスやIDの名称の重複
・wordpress標準、またはテーマのCSSによる影響
上記には注意すること
*/
</style>
</head>
<body>
<div class="game_container">
<div class="main_character_move" style="position: absolute;">
<img class="main_character" src="https://likehuman.info/wp-content/uploads/2022/11/ぴぽや_女001_勇者.png" alt="女性勇者">
</div>
</div>
<div class="control_container">
<div class="cross_key_container">
<button type="button" class="up_btn"></button>
<button type="button" class="left_btn"></button><button type="button" class="right_btn"></button>
<button type="button" class="down_btn"></button>
</div>
<div class="a_and_b_btn">
<button type="button" class="bgm_btn">BGM</button>
<button type="button" class="a_btn"> A </button>
<button type="button" class="b_btn"> B </button>
</div>
</div>
<audio src="https://likehuman.info/wp-content/uploads/2022/11/Cyber14-1.mp3" class="bgm_talk"></audio>
<audio src="https://likehuman.info/wp-content/uploads/2022/11/village.mp3" class="bgm_village"></audio>
<div class="blank"></div>
</body>
</html>
まずはプレイ画面がこちら
<div class="game_container">
<div class="main_character_move" style="position: absolute;">
<img class="main_character" src="https://likehuman.info/wp-content/uploads/2022/11/ぴぽや_女001_勇者.png" alt="女性勇者">
</div>
</div>
メインキャラクターである勇者は最初からHTML内で設定してます。サブキャラ同様jQueryでタグを生成してもよかったのですが、不慣れなので一応どちらのパターンも試しておこうということでこのようになりました。
次はコントローラーの部分です。
<div class="control_container">
<div class="cross_key_container">
<button type="button" class="up_btn"></button>
<button type="button" class="left_btn"></button><button type="button" class="right_btn"></button>
<button type="button" class="down_btn"></button>
</div>
<div class="a_and_b_btn">
<button type="button" class="bgm_btn">BGM</button>
<button type="button" class="a_btn"> A </button>
<button type="button" class="b_btn"> B </button>
</div>
</div>
ファミコンをオマージュしていますので十字キー、Aボタン、Bボタンを<button>タグでつくりました。……やはり、なかなか滑らかに動いてくれませんね(p_-)
jQuery
jQueryの全文です。結構長いです。コメントの書き方がいい加減です!(^^)!
<script type="text/javascript">
/*
レトロRPG(ドットによるゲーム)
・フィールドを歩く
・サブキャラとの会話
・BGMを流す
*/
jQuery(document).ready(function ($) {
/*
メンキャラに関する変数
基本的には「ぴぽや https://pipoya.net/」様からダウンロードしたキャラクターチップでスプライトを生成
するための座標を格納
*/
let mainchara_rect_top = 32;
let mainchara_rect_right = 32;
let mainchara_rect_bottom= 64;
let mainchara_rect_left = 0;
let mainchara_position_left = 100;
let mainchara_position_top = 100;
let stop_up_mainchar_id = 0;
let stop_left_mainchar_id = 0;
let stop_right_mainchar_id = 0;
let stop_down_mainchar_id = 0;
let stop_move_mainchar_id = 0;
let get_position_maincharacter;
let conversationv_flag = false;
/*
サブキャラに関する変数
基本的には「ぴぽや https://pipoya.net/」様からダウンロードしたキャラクターチップでスプライトを生成
するための座標を格納
*/
let character_position_left_1 = [50,140,170];
let character_position_top_1 = [50,140,170];
let stop_move_id_1 = [0,0,0];
let stop_chara_id_1 = [0,0,0];
let stop_walk_id_1 = [0,0,0];
let character_rect_1 = [[32,32,0,64],[32,32,0,64],[32,32,0,64]];//上、右、左、下
let get_position_character_1 = [];
/*
共通して使用する変数
・キャラクターチップの切り抜き割合
・当たり判定
・会話可能範囲判定
・BGMフラグ(使用した音素材:OtoLogic(https://otologic.jp))
・描画範囲
・キャラクターチップの設定
*/
const cut_percentage = 32;
let hit_flag = [false,false,false];
let talk_range = [false,false,false];
let hit_range = [[false,false,false,false],[false,false,false,false],[false,false,false,false]];
let bgm_village_flag = false;
let view_w = $(".game_container").width() - 70;
let view_h = $(".game_container").height() - 70;
const chara_1 = ' style="position: absolute;" name="盗賊男" src="https://likehuman.info/wp-content/uploads/2022/11/ぴぽや_男005_盗賊.png" >';
const chara_2 = ' style="position: absolute;" name="メイド女性" src="https://likehuman.info/wp-content/uploads/2022/11/ぴぽや_女006_メイド.png">';
const chara_3 = ' style="position: absolute;" name="子ども女" src="https://likehuman.info/wp-content/uploads/2022/11/ぴぽや_子供_女001_一般人.png">';
//サブキャラのシャッフル
let character_set_data = (array) => {
let character_list = [...array];
for (let i = character_list.length - 1; i >= 0; i--) {
let random_num = Math.floor(Math.random() * (i + 1));
let tmp_data = character_list[i];
character_list[i] = character_list[random_num];
character_list[random_num] = tmp_data;
}
return character_list;
}
const new_character_list = character_set_data([chara_1,chara_2,chara_3]);
//サブキャラの向きと初期位置の設定
$.each(new_character_list, function(i, value) {
$('.main_character_move').after('<div class="character_move_'+ i +'" style="position: absolute;"><img class="character_' + i +'"' + new_character_list[i] + '</div>');
$('.character_'+i).css({
'left' : character_position_left_1[i] +'px',
'top' : character_position_top_1[i] + 'px',
'clip':'rect('+
character_rect_1[i][0] + 'px,' +
character_rect_1[i][1] + 'px,' +
character_rect_1[i][3] + 'px,' +
character_rect_1[i][2] + 'px' +
')'
});
})
//メインキャラの初期位置を設定
$(".main_character_move").css({
'left' : mainchara_position_left +'px',
'top' : mainchara_position_top + 'px'
});
//メインキャラの位置情報を取得
get_position_maincharacter = $(".main_character_move").offset();
//サブキャラの位置情報を取得
$.each(new_character_list, function(i, value) {
get_position_character_1[i] = $(".character_move_"+i).offset();
//console.log(get_position_character_1[i]);
})
//メインキャラのムーブを開始
mainCharaMove();
//サブキャラのムーブを開始
$.each(new_character_list, function(i, value) {
charaDirectionDecide_1(i);
})
/*
サブキャラの向きを設定
・サブキャラの向きを2秒間隔でランダムに変更
*/
function charaDirectionDecide_1(i){
let rand_num = Math.floor(Math.random()*4);
characterInstruct_1(i,rand_num);
stop_chara_id_1[i] = setTimeout(charaDirectionDecide_1, 2000,i);
}
/*
サブキャラの向きに応じた処理の実行
*/
function characterInstruct_1(i,ran){
switch (ran){
case 0 ://下向き
clearTimeout(stop_move_id_1[i]);
clearTimeout(stop_walk_id_1[i]);
character_rect_1[i][0] = 0;
character_rect_1[i][3] = 32;
characterMove_1(i);
characterWalkDown_1(i);
break;
case 1 ://左向き
clearTimeout(stop_move_id_1[i]);
clearTimeout(stop_walk_id_1[i]);
character_rect_1[i][0] = 32;
character_rect_1[i][3] = 64;
characterMove_1(i);
characterWalkLeft_1(i);
break;
case 2 ://右向き
clearTimeout(stop_move_id_1[i]);
clearTimeout(stop_walk_id_1[i]);
character_rect_1[i][0] = 64;
character_rect_1[i][3] = 96;
characterMove_1(i);
characterWalkRight_1(i);
break;
case 3 ://上向き
clearTimeout(stop_move_id_1[i]);
clearTimeout(stop_walk_id_1[i]);
character_rect_1[i][0] = 96;
character_rect_1[i][3] = 128;
characterMove_1(i);
characterWalkUp_1(i);
break;
default:
break;
}
}
/*
サブキャラの向きに応じてキャラクターチップからスプライトを生成する
*/
function characterMove_1(i){
stop_move_id_1[i] = setTimeout(characterMove_1, 200,i);
//character_rect_1 = [32,32,0,64];//上、右、左、下
$(".character_"+i).css({
'top' : - character_rect_1[i][0] + 'px',
'left': - character_rect_1[i][2] + 'px',
'clip':'rect('+
character_rect_1[i][0] + 'px,' +
character_rect_1[i][1] + 'px,' +
character_rect_1[i][3] + 'px,' +
character_rect_1[i][2] + 'px' +
')'
});
character_rect_1[i][1] += cut_percentage;//右
character_rect_1[i][2] += cut_percentage;//左
if(character_rect_1[i][1] == 128){
character_rect_1[i][1] = 32;
character_rect_1[i][2] = 0;
}
}
/*
サブキャラを歩かせる
・setTimeout関数で歩く速度を調整
・歩く毎に位置を取得し、メインキャラとの当たり判定をチェックする
・当たり判定があり、かつメインキャラが進行方向に存在した場合は歩くのを止める
・当たり判定はあるが、進行方向にメインキャラが存在しなければ歩くのを継続する
・以上をsetTimeoutで50ミリ秒間隔で繰り返す
*/
//サブキャラを下方向に歩かせる
function characterWalkDown_1(i){
stop_walk_id_1[i] = setTimeout(characterWalkDown_1, 50,i);
get_position_character_1[i] = $(".character_move_"+i).offset();
hitJudgment(i);
let hit = [false,false,false,false,false];
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
if(character_position_top_1[i] <= view_h && !hit[3]){
character_position_top_1[i] += 1;
}
$(".character_move_"+i).css({
'left' : character_position_left_1[i] + 'px',
'top' : character_position_top_1[i] + 'px'
});
}
//サブキャラを左方向に歩かせる
function characterWalkLeft_1(i){
stop_walk_id_1[i] = setTimeout(characterWalkLeft_1, 50,i);
get_position_character_1[i] = $(".character_move_"+i).offset();
hitJudgment(i);
let hit = [false,false,false,false,false];
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
if(character_position_left_1[i] >= 0 && !hit[2]){
character_position_left_1[i] -= 1;
}
$(".character_move_"+i).css({
'left' : character_position_left_1[i] + 'px',
'top' : character_position_top_1[i] + 'px'
});
}
//サブキャラを右方向に歩かせる
function characterWalkRight_1(i){
stop_walk_id_1[i] = setTimeout(characterWalkRight_1, 50,i);
get_position_character_1[i] = $(".character_move_"+i).offset();
hitJudgment(i);
let hit = [false,false,false,false,false];
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
if(character_position_left_1[i] <= view_w && !hit[1]){
character_position_left_1[i] += 1;
}
$(".character_move_"+i).css({
'left' : character_position_left_1[i] + 'px',
'top' : character_position_top_1[i] + 'px'
});
}
//サブキャラを上方向に歩かせる
function characterWalkUp_1(i){
stop_walk_id_1[i] = setTimeout(characterWalkUp_1, 50,i);
get_position_character_1[i] = $(".character_move_"+i).offset();
hitJudgment(i);
let hit = [false,false,false,false,false];
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
if(character_position_top_1[i] >= 10 && !hit[4]){
character_position_top_1[i] -= 1;
}
$(".character_move_"+i).css({
'left' : character_position_left_1[i] + 'px',
'top' : character_position_top_1[i] + 'px'
});
}
/****************************************************
メインキャラの向きと身体の動き
・キャラクターチップを切り抜き、スプライトを生成する
・setTimeout関数でスプライトに動きをつける
*****************************************************/
function mainCharaMove(){
stop_move_mainchar_id = setTimeout(mainCharaMove, 200);
$(".main_character").css({
'top' : - mainchara_rect_top + 'px',
'left': - mainchara_rect_left + 'px',
'clip':'rect('+
mainchara_rect_top + 'px,' +
mainchara_rect_right + 'px,' +
mainchara_rect_bottom + 'px,' +
mainchara_rect_left + 'px' +
')'
});
mainchara_rect_right += cut_percentage;
mainchara_rect_left += cut_percentage;
if(mainchara_rect_right == 128){
mainchara_rect_right = 32;
mainchara_rect_left = 0;
}
}
//十字キーの下ボタンを押下したとき
$(".down_btn").on('mousedown touchstart', function(){
clearTimeout(stop_move_mainchar_id);
//会話を止める
if(conversationv_flag)endOfConversation();
//下を向いたキャラクター画像をキャラクターチップから切り抜くための座標設定
mainchara_rect_top = 0;
mainchara_rect_bottom = 32;
//切り抜いた画像に動きをつける
mainCharaMove();
//下に歩かせる
mainCharaWalkDown();
});
//十字キーの左ボタンを押下したとき
$(".left_btn").on('mousedown touchstart', function(){
clearTimeout(stop_move_mainchar_id);
if(conversationv_flag)endOfConversation();
mainchara_rect_top = 32;
mainchara_rect_bottom = 64;
mainCharaMove();
mainCharaWalkLeft();
});
//十字キーの右ボタンを押下したとき
$(".right_btn").on('mousedown touchstart', function(){
clearTimeout(stop_move_mainchar_id);
if(conversationv_flag)endOfConversation();
mainchara_rect_top = 64;
mainchara_rect_bottom = 96;
mainCharaMove();
mainCharaWalkRight();
});
//十字キーの上ボタンを押下したとき
$(".up_btn").on('mousedown touchstart', function(){
clearTimeout(stop_move_mainchar_id);
if(conversationv_flag)endOfConversation();
mainchara_rect_top = 96;
mainchara_rect_bottom = 128;
mainCharaMove();
mainCharaWalkUp();
});
//十字キーを離すと同時にsettimeout関数を止め、歩くのを止めさせる
$(".down_btn").on('mouseup mouseleave touchend', function(){
clearTimeout(stop_down_mainchar_id);
});
$(".left_btn").on('mouseup mouseleave touchend', function(){
clearTimeout(stop_left_mainchar_id);
});
$(".right_btn").on('mouseup mouseleave touchend', function(){
clearTimeout(stop_right_mainchar_id);
});
$(".up_btn").on('mouseup mouseleave touchend', function(){
clearTimeout(stop_up_mainchar_id);
});
/****************************************************
メインキャラを歩かせる
・setTimeout関数で歩く速度を調整
・歩く毎に位置を取得し、サブキャラとの当たり判定をチェックする
・当たり判定があり、かつサブキャラが進行方向に存在した場合は歩くのを止める
・当たり判定はあるが、進行方向にサブキャラが存在しなければ歩くのを継続する
*****************************************************/
//メインキャラを下方向に歩かせる
function mainCharaWalkDown(){
let hit = [false,false,false,false,false];
get_position_maincharacter = $(".main_character_move").offset();
$.each(new_character_list, function(i, value) {
hitJudgment(i);
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
});
if(mainchara_position_top <= view_h && !hit[4])mainchara_position_top += 1;
stop_down_mainchar_id = setTimeout(() => {
mainCharaWalkDown();
},1);
mainCharaPosition();
}
//メインキャラを左方向に歩かせる
function mainCharaWalkLeft(){
let hit = [false,false,false,false,false];
get_position_maincharacter = $(".main_character_move").offset();
$.each(new_character_list, function(i, value) {
hitJudgment(i);
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
});
if(mainchara_position_left >= 0 && !hit[1])mainchara_position_left -= 1;
stop_left_mainchar_id = setTimeout(() => {
mainCharaWalkLeft();
},1);
mainCharaPosition();
}
//メインキャラを右方向に歩かせる
function mainCharaWalkRight(){
let hit = [false,false,false,false,false];
get_position_maincharacter = $(".main_character_move").offset();
$.each(new_character_list, function(i, value) {
hitJudgment(i);
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
});
if(mainchara_position_left <= view_w && !hit[2])mainchara_position_left += 1;
stop_right_mainchar_id = setTimeout(() => {
mainCharaWalkRight();
},1);
mainCharaPosition();
}
//メインキャラを上方向に歩かせる
function mainCharaWalkUp(){
let hit = [false,false,false,false,false];
get_position_maincharacter = $(".main_character_move").offset();
$.each(new_character_list, function(i, value) {
hitJudgment(i);
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
});
if(mainchara_position_top >= 10 && !hit[3])mainchara_position_top -= 1;
stop_up_mainchar_id = setTimeout(() => {
mainCharaWalkUp();
},1);
mainCharaPosition();
}
//メインキャラの描画(歩かせる)
function mainCharaPosition(){
$(".main_character_move").css({
'left' : mainchara_position_left + 'px',
'top' : mainchara_position_top + 'px'
});
}
/****************************************************
当たり判定
・メインキャラの半径30px範囲内にサブキャラが存在しているかどうか
・存在している(当たっている)場合は、その対象(サブキャラ)が存在している方角の確認
*****************************************************/
function hitJudgment(i){
let main_left = get_position_maincharacter.left;
let main_top = get_position_maincharacter.top;
let chara_1_left = get_position_character_1[i].left;
let chara_1_top = get_position_character_1[i].top;
let hit_more_once = false;
//当たり判定
if(main_left >= chara_1_left - 30 && main_left <= chara_1_left + 30 && main_top >= chara_1_top - 30 && main_top <= chara_1_top + 30){
//当たっているかどうか
hit_flag[i] = true;
//サブキャラがメインキャラの左にいる
if(main_left >= chara_1_left)hit_range[i][0] = true;
//サブキャラがメインキャラの右にいる
if(main_left <= chara_1_left)hit_range[i][1] = true;
//サブキャラがメインキャラの上にいる
if(main_top >= chara_1_top)hit_range[i][2] = true;
//サブキャラがメインキャラの下にいる
if(main_top <= chara_1_top)hit_range[i][3] = true;
}else{//当たり判定がなければ初期化
hit_flag[i] = false;
hit_range[i][0] = false;
hit_range[i][1] = false;
hit_range[i][2] = false;
hit_range[i][3] = false;
}
//会話範囲判定(会話できる距離にサブキャラがいるかどうか)
if(main_left >= chara_1_left - 60 && main_left <= chara_1_left + 60 && main_top >= chara_1_top - 60 && main_top <= chara_1_top + 60){
talk_range[i] = true;
}else{
talk_range[i] = false;
}
}
/*****************************************************
会話処理
・会話範囲判定がある場合は、その対象(サブキャラ)の属性を取得
・属性に応じたセリフの設定
・会話中はサブキャラ全員の動き(スレッド)を止める
・会話タグおよびCSSを500ミリ秒かけて出力
*****************************************************/
function conversation(chara_num){
//会話フラグ(連続で会話するのを防ぐため)
conversationv_flag = true;
//会話対象キャラを初期化
talk_target = 99;
//キャラ属性を取得
let chara_type = $('.character_' + chara_num).attr("name");
let dialog = "";
//メインキャラ以外の動きを止める(スレッド処理を止める)
$.each(new_character_list, function(i, value) {
clearTimeout(stop_chara_id_1[i]);
clearTimeout(stop_walk_id_1[i]);
clearTimeout(stop_move_id_1[i]);
});
//キャラに対応したセリフの設定
if(chara_type === "盗賊男")dialog = "村を出るとモンスターがウジャウジャいるぜ!気を付けな!";
if(chara_type === "メイド女性")dialog = "ここは初心者の村です。ゆっくりしていってくださいね";
if(chara_type === "子ども女")dialog = "jQueryって知ってる? 難しいけど楽しいよね!";
$('.game_container').after('<div class ="conversation_container"><div class="conversation" value="">' + dialog + '</div></div>');
$('.conversation').show().animate({
//色などの複雑な設定はできないので、初期設定はCSS
top: 0,
left: 0,
width: '100%',
height: '100px'
}, 500);
$(".bgm_talk").get(0).play();
}
/*****************************************************
終話処理
会話タグおよびCSSを削除
サブキャラの動き(スレッド)を再開
******************************************************/
function endOfConversation(){
$(".conversation").remove();
$(".conversation_container").remove();
conversationv_flag = false;
//メインキャラ以外の動きを再開する
$.each(new_character_list, function(i, value) {
charaDirectionDecide_1(i);
})
}
//BGMを再生
//効果音提供 オトロジック(https://otologic.jp)
$(".bgm_btn").on('click',function(){
$(".bgm_village").get(0).onplaying = function() {
bgm_village_flag = true;
};
$(".bgm_village").get(0).onpause = function() {
bgm_village_flag = false;
};
if(!bgm_village_flag){
$(".bgm_village").get(0).play();
//連続再生
$(".bgm_village").get(0).loop = true;
}else{
$(".bgm_village").get(0).pause();
}
});
//Bボタンで会話をキャンセル
$(".b_btn").on('click',function(){
if(conversationv_flag)endOfConversation();
});
//Aボタンで会話範囲判定内であれば会話する
$(".a_btn").on('click',function(){
let talk_target = 99;
//会話範囲判定の検索
$.each(new_character_list, function(i, value) {
if(talk_range[i])talk_target = i;
});
//会話範囲判定内であれば会話する
if(talk_target !== 99){
if(conversationv_flag){
endOfConversation();
}else{
conversation(talk_target);
}
}
});
});
</script>
まず、メインキャラやサブキャラなどの画像データ(キャラクターチップ)は「ぴぽや」様からダウンロードさせて頂きました。
簡単に概要を説明するとこんな感じです。
1、サブキャラクターの画像データを配列に格納。更にその配列の中身をシャッフルすることで、ブラウザが更新されるたびに、サブキャラの配置もランダムに
↓
2、一定の間隔でサブキャラの向きをランダムで変更する
↓
3、向きに対応した体の動きと歩行を描画
↓
4、メインキャラとの当たり判定
↓
5、当たり判定があれば歩行停止
↓
6、当たり判定があれば会話可能
↓
7、会話の際、一瞬だけ効果音を流し、会話中はサブキャラ全員の動き(settimeout関数によるスレッド)を停止させる
とまぁ、ざっくりとこんな感じですね。
サブキャラの初期位置をランダムにする
const chara_1 = ' style="position: absolute;" name="盗賊男" src="https://likehuman.info/wp-content/uploads/2022/11/ぴぽや_男005_盗賊.png" >';
const chara_2 = ' style="position: absolute;" name="メイド女性" src="https://likehuman.info/wp-content/uploads/2022/11/ぴぽや_女006_メイド.png">';
const chara_3 = ' style="position: absolute;" name="子ども女" src="https://likehuman.info/wp-content/uploads/2022/11/ぴぽや_子供_女001_一般人.png">';
//サブキャラのシャッフル
let character_set_data = (array) => {
let character_list = [...array];
for (let i = character_list.length - 1; i >= 0; i--) {
let random_num = Math.floor(Math.random() * (i + 1));
let tmp_data = character_list[i];
character_list[i] = character_list[random_num];
character_list[random_num] = tmp_data;
}
return character_list;
}
const new_character_list = character_set_data([chara_1,chara_2,chara_3]);
サブキャラが格納されている配列の中身をシャッフルすることで、ブラウザを更新するたびに、サブキャラの配置を変更するようにしました。ちなみに配列のシャッフルにはFisher–Yates shuffleのアルゴリズムを使用しています。
今回は3体しか使っていませんが、何体でも可能ですよ!(^^)!
一定間隔でサブキャラの向きを変更する
function charaDirectionDecide_1(i){
let rand_num = Math.floor(Math.random()*4);
characterInstruct_1(i,rand_num);
stop_chara_id_1[i] = setTimeout(charaDirectionDecide_1, 2000,i);
}
2秒ごとに向きを変えるようにしています。更に言えば2秒ごとにランダムでサブキャラの向きを変え、歩かせるよう指示を出しています。
向きに対応した体の動きと歩行を描画
向きに対応した体の動きですが、下記のようにして切り抜き、setTimeout関数で切り抜いたキャラクター画像を一定の間隔で切り替えながら、動いているように描画しています(スプライトの生成)。この処理はメインキャラクターもサブキャラクターも同様です。
function characterMove_1(i){
stop_move_id_1[i] = setTimeout(characterMove_1, 200,i);
//character_rect_1 = [32,32,0,64];//上、右、左、下
$(".character_"+i).css({
'top' : - character_rect_1[i][0] + 'px',
'left': - character_rect_1[i][2] + 'px',
'clip':'rect('+
character_rect_1[i][0] + 'px,' +
character_rect_1[i][1] + 'px,' +
character_rect_1[i][3] + 'px,' +
character_rect_1[i][2] + 'px' +
')'
});
character_rect_1[i][1] += cut_percentage;//右
character_rect_1[i][2] += cut_percentage;//左
if(character_rect_1[i][1] == 128){
character_rect_1[i][1] = 32;
character_rect_1[i][2] = 0;
}
}
下記は歩行の部分です。
//サブキャラを下方向に歩かせる
function characterWalkDown_1(i){
stop_walk_id_1[i] = setTimeout(characterWalkDown_1, 50,i);
get_position_character_1[i] = $(".character_move_"+i).offset();
hitJudgment(i);
let hit = [false,false,false,false,false];
if(hit_flag[i]){
hit[0] = true;//当たっているか
hit[1] = hit_range[i][0];//x座標→メインキャラの方が数値大
hit[2] = hit_range[i][1];//x座標→メインキャラの方が数値小
hit[3] = hit_range[i][2];//y座標→メインキャラの方が数値大
hit[4] = hit_range[i][3];//y座標→メインキャラの方が数値小
}
if(character_position_top_1[i] <= view_h && !hit[3]){
character_position_top_1[i] += 1;
}
$(".character_move_"+i).css({
'left' : character_position_left_1[i] + 'px',
'top' : character_position_top_1[i] + 'px'
});
}
上記は歩行の部分ですが、メインキャラもサブキャラも基本的には同じ処理です。
まずはsetTimeout関数で歩く速度を調整。歩幅は1px単位
↓
歩く毎に位置を取得し、メインキャラとの当たり判定をチェックする
↓
当たり判定があり、かつメインキャラが進行方向に存在した場合は歩くのを止める
↓
当たり判定はあるが、進行方向にメインキャラが存在しなければ歩くのを継続する
↓
以上をsetTimeoutで50ミリ秒間隔で繰り返す
こんな感じです。
当たり判定
function hitJudgment(i){
let main_left = get_position_maincharacter.left;
let main_top = get_position_maincharacter.top;
let chara_1_left = get_position_character_1[i].left;
let chara_1_top = get_position_character_1[i].top;
let hit_more_once = false;
//当たり判定
if(main_left >= chara_1_left - 30 && main_left <= chara_1_left + 30 && main_top >= chara_1_top - 30 && main_top <= chara_1_top + 30){
//当たっているかどうか
hit_flag[i] = true;
//サブキャラがメインキャラの左にいる
if(main_left >= chara_1_left)hit_range[i][0] = true;
//サブキャラがメインキャラの右にいる
if(main_left <= chara_1_left)hit_range[i][1] = true;
//サブキャラがメインキャラの上にいる
if(main_top >= chara_1_top)hit_range[i][2] = true;
//サブキャラがメインキャラの下にいる
if(main_top <= chara_1_top)hit_range[i][3] = true;
}else{//当たり判定がなければ初期化
hit_flag[i] = false;
hit_range[i][0] = false;
hit_range[i][1] = false;
hit_range[i][2] = false;
hit_range[i][3] = false;
}
//会話範囲判定(会話できる距離にサブキャラがいるかどうか)
if(main_left >= chara_1_left - 60 && main_left <= chara_1_left + 60 && main_top >= chara_1_top - 60 && main_top <= chara_1_top + 60){
talk_range[i] = true;
}else{
talk_range[i] = false;
}
}
上記は当たり判定処理の部分です。
概要としては、メインキャラの半径30px範囲内にサブキャラが存在しているかどうか。そして存在している(当たっている)場合は、さらにメインキャラを中心とした、大体のサブキャラ位置を取得するようにしています。理由はメインキャラをスムーズに歩かせるためです。当たり判定がある場合、歩行を停止するようにしていますが、単なる当たり判定だけでは接触するたびに動かせなくなってしまうので。
ついでに会話範囲判定も取得するようにしています。半径60px範囲内にサブキャラが存在するときは会話フラグ(talk_range)をtrueにします。
会話する
function conversation(chara_num){
//会話フラグ(連続で会話するのを防ぐため)
conversationv_flag = true;
//会話対象キャラを初期化
talk_target = 99;
//キャラ属性を取得
let chara_type = $('.character_' + chara_num).attr("name");
let dialog = "";
//メインキャラ以外の動きを止める(スレッド処理を止める)
$.each(new_character_list, function(i, value) {
clearTimeout(stop_chara_id_1[i]);
clearTimeout(stop_walk_id_1[i]);
clearTimeout(stop_move_id_1[i]);
});
//キャラに対応したセリフの設定
if(chara_type === "盗賊男")dialog = "村を出るとモンスターがウジャウジャいるぜ!気を付けな!";
if(chara_type === "メイド女性")dialog = "ここは初心者の村です。ゆっくりしていってくださいね";
if(chara_type === "子ども女")dialog = "jQueryって知ってる? 難しいけど楽しいよね!";
$('.game_container').after('<div class ="conversation_container"><div class="conversation" value="">' + dialog + '</div></div>');
$('.conversation').show().animate({
//色などの複雑な設定はできないので、初期設定はCSS
top: 0,
left: 0,
width: '100%',
height: '100px'
}, 500);
$(".bgm_talk").get(0).play();
}
先ほどの会話フラグ(talk_range)がtrueの時は、Aボタンで会話範囲判定内であれば上記(conversation)を実行し、会話するようにしています。また、会話の冒頭で効果音を流しています(音素材は『OtoLogic(https://otologic.jp)』様から利用させていただいております)。下記はconversationの概要です。
会話範囲判定がある場合は、その対象(サブキャラ)の属性を取得
↓
属性に応じたセリフの設定
↓
会話中はサブキャラ全員の動き(スレッド)を止める
↓
会話タグおよびCSSを500ミリ秒かけて出力
以上がこのゲームの主な仕様になります。
このゲームでもやはりsetTimeout関数を多用しています。というより、setTimeoutが無ければキャラクターは動きません(笑)。
setTimeout関数については下記の記事をご覧ください。
ドットゲーム好きで、かつプログラマーを目指している方は是非、つくってみてください\(^o^)/
楽しく学べますし、何より一番手っ取り早く身に付く学習法だと思いますよ~
どこでも食っていけるWeb人間になれる【Web食いオンラインスクール】