/** * 2次元ベクトルのクラス */ class Vec2 { /** * @param {number} x成分 * @param {number} y成分 */ constructor(x, y) { this.x = x; this.y = y; } /** * @param {Vec2} b 足したいベクトル */ add(b) { let a = this; return new Vec2(a.x+b.x, a.y+b.y); } /** * @param {Vec2} b 引きたいベクトル */ sub(b) { let a = this; return new Vec2(a.x-b.x, a.y-b.y); } /** * @param {number} s ベクトルにかけたい実数 */ mul(s) { return new Vec2(s*this.x, s*this.y); } /** * @param {number} s この実数でベクトルを割る */ div(s) { return new Vec2(this.x/s, this.y/s); } /** * @param {Vec2} v このベクトルとドット積をとる */ dot(b) { let a = this; return a.x*b.x + a.y*b.y; } /** * @returns ベクトルの大きさ(成分のユークリッド距離) */ mag() { return sqrt(this.x ** 2 + this.y ** 2); } /** * @param {number} s 大きさをsとしたベクトルを返す */ magSet(s) { return this.mul(s/this.mag()); } /** * @param {number} s 大きさにsを足したベクトルを返す */ magAdded(s) { return this.mul(1+s/this.mag()); } /** * @param {number} rad 回転させたい角度。単位はラジアン。 */ rotated(rad) { return new Vec2( this.x*cos(rad) - this.y*sin(rad), this.x*sin(rad) + this.y*cos(rad) ); } /** * @returns 正規化されたベクトル */ normalized() { return this.mul(1/this.mag()); } copy() { return new Vec2(this.x, this.y); } /** * @param {Vec2} b このベクトルと成分が等しいか否かを返す */ equals(b) { let a = this; return a.x === b.x && a.y === b.y; } } class Camera { constructor(pos, angle) { this.pos = pos; this.angle = angle; } } class Sankaku{ constructor(){ this.V = [ [0, 0, 0], [1, 0, 0], [0, 0, 1], [0, -1, 0] ]; this.F = [ [0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2] ]; } draw(){ let ft = (A,x,y,z) => A.map(a => [a[0]+x, a[1]+y, a[2]+z]); let fs = (A,x,y,z) => A.map(a => [x*a[0], y*a[1], z*a[2]]); let fr = (A,r) => A.map( a => [ a[0]*cos(r) + a[2]*sin(r), a[1], -a[0]*sin(r) + a[2]*cos(r) ] ); let fp = (A) => A.map(a => [a[0]/a[2], a[1]/a[2]]); let fc = (A,w,h) => A.map(a => [width*a[0]+width/2, height*a[1]+height/2]); let t = millis()/1000; let S = this.V; // モデルを回転&平行移動 S = fr(S, 2*t); S = ft(S, 2, 1, 1);//0,1,1 // カメラに応じて平行移動&回転 S = ft(S, -game.camera.pos[0], -game.camera.pos[1],-game.camera.pos[2]); S = fr(S, -game.camera.angle); // 投影面の後ろにあるときは描画しない if (S[0][2] < 1) return; // 投影 S = fp(S); S = fc(S, width, height); strokeWeight(3); fill('blue'); for(let i=0; i A.map(a => [a[0]+x, a[1]+y, a[2]+z]); let fs = (A,x,y,z) => A.map(a => [x*a[0], y*a[1], z*a[2]]); let fr = (A,r) => A.map( a => [ a[0]*cos(r) + a[2]*sin(r), a[1], -a[0]*sin(r) + a[2]*cos(r) ] ); let fp = (A) => A.map(a => [a[0]/a[2], a[1]/a[2]]); let fc = (A,w,h) => A.map(a => [width*a[0]+width/2, height*a[1]+height/2]); let t = millis()/1000; let S = this.V; // モデルを回転&平行移動 S = fr(S, 2*t); S = ft(S,this.sankakuX,this.sankakuY,this.sankakuZ);//S = ft(S, 0, 1, 1); 5,1,1//this.sankakuX,this.sankakuY,this.sankakuZ // カメラに応じて平行移動&回転 S = ft(S, -game.camera.pos[0], -game.camera.pos[1],-game.camera.pos[2]); S = fr(S, -game.camera.angle); this.vertsInCamCoord = ft(S, 0, 0, 0); // 投影面の後ろにあるときは描画しない if (S[0][2] < 1) return; // 投影 S = fp(S); S = fc(S, width, height); this.S = S; } draw() { fill(this.sankakuColor); let S = this.S; if (S.length === 0) return; for(let i=0; i ((a+0.0001+10000*PI) % (2*PI)); let isFacingUs = a => (a = normalized(a), (a >= -PI/36 && a <= 37*PI/36)); let isFacingUsNarrow = a => (a = normalized(a), (a >= PI/12 && a <= 11*PI/12)); angle = normalized(angle); let shake = new Vec2(0, ((animFrame % 20) > 10) ? -2 : 0); let bodyCenter = pos.add(new Vec2(0,-16)).add(shake); let assCenter = isFacingUsNarrow(angle+PI) ? bodyCenter.add(new Vec2(12*cos(angle+PI), -4)) : null; let headCenter = bodyCenter.add(new Vec2(6*cos(angle),-16)).sub(shake); let beakCenter = isFacingUs(angle) ? headCenter.add(new Vec2(12*cos(angle),0)) : null; let eyes = []; if (isFacingUs(angle-PI/3)) eyes.push(headCenter.add(new Vec2(8*cos(angle-PI/3),-4))); if (isFacingUs(angle+PI/3)) eyes.push(headCenter.add(new Vec2(8*cos(angle+PI/3),-4))); push(); stroke('black'); strokeWeight(2); fill('yellow'); let drawBody = _=>circle(bodyCenter.x, bodyCenter.y, 32); let drawHead = _=>circle(headCenter.x, headCenter.y, 24); if (isFacingUs(angle)) { drawBody();drawHead(); } else { drawHead();drawBody(); } fill('black'); strokeWeight(0); for(let eye of eyes){ circle(eye.x, eye.y, 3); } fill('orange') strokeWeight(1); if (beakCenter !== null) { circle(beakCenter.x, beakCenter.y, 8); } fill('black'); textAlign(CENTER, CENTER); textSize(10); strokeWeight(1); if (assCenter !== null) { text('x', assCenter.x, assCenter.y); } pop(); } } class Actor { constructor() { this.x = 0; this.y = 0; this.z = 0; this.angle = 0; } } class Game { constructor() { //----- ゲームの舞台を生成 this.field = [ [0,0,0,0,0,0,0,0,0,0,0,0,0], [0,0,1,1,1,0,0,0,0,0,0,0,0], [0,2,1,0,0,1,1,0,0,0,0,0,0], [0,1,3,0,0,0,0,1,1,1,0,0,0], [0,1,0,0,0,0,0,0,0,0,1,1,0], [0,1,0,0,0,0,0,1,0,0,0,1,0], [0,1,0,0,2,1,1,0,1,0,2,1,0], [0,1,1,1,1,3,0,0,0,1,1,3,0], [0,0,0,0,0,0,0,0,0,0,0,0,0], ]; // 地形のカメラ計算が不完全(y座標が0のときだけ正しく動く) // ので注意 this.camera = new Camera([3, 0, -0.75], 15*PI*2/16); this.hiyoko = new Actor(); this.hiyoko.x = 1.5; this.hiyoko.y = 1; this.hiyoko.z = 1.5; } /** * 毎フレーム行う処理 */ proc(){ // プログラム再生してからの経過秒数 let t = millis()/1000; // WSADで操作 if (key === 'a') { game.hiyoko.angle -= 1/10; }else if (key === 'd') { game.hiyoko.angle += 1/10; }else{ let inputVec = ( !keyIsPressed ? [0, 0] : key === 'w' ? [0, 1] : key === 's' ? [0, -1] : [0, 0] ); let walkVec = inputVec; let x = inputVec[0]; let z = inputVec[1]; let r = game.hiyoko.angle; walkVec[0] = z*sin(r) + x*cos(r); walkVec[1] = z*cos(r) - x*sin(r); game.hiyoko.x += walkVec[0] / 5; game.hiyoko.z += walkVec[1] / 5; } // カメラをひよこの後ろへ let hiyokoPosX = game.hiyoko.x; let hiyokoPosZ = game.hiyoko.z; let hiyokoGyakuX = -3*sin(game.hiyoko.angle); let hiyokoGyakuZ = -3*cos(game.hiyoko.angle); let cameraPosX = hiyokoPosX + hiyokoGyakuX; let cameraPosZ = hiyokoPosZ + hiyokoGyakuZ; game.camera.pos = [cameraPosX, 0, cameraPosZ]; game.camera.angle = game.hiyoko.angle; let colorDarkgreen = [22, 100, 45]; let colorGreen = [22, 150, 45]; let colorRoad = [236, 231, 195]; // 投影面上の1点 [l,m] の明るさをの範囲で求める関数 let briFilm = (l,m) => { // 床の平面上の点[x,z]のRGBを求める関数 let briFloor = (x,z) => { let a = floor(x); let b = floor(z); if (a >= 0 && a < this.field.length && b >= 0 && b < this.field[0].length) { // フィールドの範囲内 let tile = this.field[a][b]; if(tile === 1) { return colorRoad; }else if (tile === 2) { let j = x%1; let k = z%1; if (j+k-1 > 0) return colorRoad; }else if (tile === 3) { let j = x%1; let k = z%1; if (j+k-1 < 0) return colorRoad; } return colorGreen; } // フィールドの範囲外 return colorDarkgreen; } // 床 y=1 との交点 p を求める let h = 1; let p = [l*h/m, h, h/m]; //----- カメラ情報に応じて交点を操作 let r = game.camera.angle; //y軸に対する回転量(ラジアン) // 回転させてから let x = p[0]; let z = p[2]; p[0] = z*sin(r) + x*cos(r); p[2] = z*cos(r) - x*sin(r); // 平行移動 p[0] += this.camera.pos[0]; p[2] += this.camera.pos[2]; //----- // 交点pの床の模様を、関数の値(計算結果)とする return briFloor(p[0], p[2]); }; //----- 描画 background(12, 178, 227); noStroke(); let pixelSize = 6; for(let y=height/2; y 1) { let playerDrawer = new ChickenDrawer( new Vec2(width * x3/z3 + width/2, height * y3/z3 + height/2), game.hiyoko.angle - game.camera.angle - 2*PI / 4, 0 //floor(player.pos.x) + floor(player.pos.y) ); playerDrawer.draw(); } // 三角錐の配列 let sankakuList = []; // 三角錐の配列 let sankakuSitaList = []; // 影の配列 let shadowList = []; for(let i=0; i<5; i++){ let color; if(i%2==0){ color='orange' }else{ color='yellow' } // 三角錐を生成 let sankaku = new SankakuTry(); sankaku.sankakuX = 2 + 1.5*i; sankaku.sankakuY = 0 + sin(2*t); sankaku.sankakuZ = 3; sankaku.sankakuColor = color; sankaku.proc(); sankakuList.push(sankaku); // 影を生成 let shadow = new SankakuTry(); shadow.sankakuX = 2 + 1.5*i; shadow.sankakuY = 1; shadow.sankakuZ = 3; shadow.sankakuColor = 'black'; shadow.V = shadow.V.map(pos => [pos[0], 0, pos[2]]); shadow.proc(); shadowList.push(shadow); // 三角錐下側_Aを生成 let sankakuSitaA = new SankakuTry(); sankakuSitaA.sankakuX = 2 + 1.5*i; sankakuSitaA.sankakuY = 1;//-1 sankakuSitaA.sankakuZ = 9; sankakuSitaA.sankakuColor = color; sankakuSitaA.proc(); sankakuList.push(sankakuSitaA); } //赤い三角錐 let sankakuSitaC = new SankakuTry(); sankakuSitaC.sankakuX = 2 + 1.5*3; sankakuSitaC.sankakuY = 1; sankakuSitaC.sankakuZ = 15 - t; sankakuSitaC.sankakuColor = 'red'; //衝突判定のテスト for(let sankaku of sankakuList) { // 球の中心座標 let v = [ sankakuSitaC.sankakuX, sankakuSitaC.sankakuY, sankakuSitaC.sankakuZ ]; let w = [ sankaku.sankakuX, sankaku.sankakuY, sankaku.sankakuZ, ]; // 半径(仮) let a = 0.5; let b = 0.5; // 2つの球の中心間の距離 let d = sqrt((v[0]-w[0])**2 + (v[1]-w[1])**2 + (v[2]-w[2])**2); // 半径の和より小さいなら衝突している if (d < a+b) { sankakuSitaC.sankakuColor = 'blue'; break; } } sankakuSitaC.proc(); sankakuList.push(sankakuSitaC); // Zソートで三角錐の描画順を決定 // カメラ座標系でのz座標でソートする sankakuList.sort( (san1, san2) => { let z1 = san1.vertsInCamCoord[0][2]; let z2 = san2.vertsInCamCoord[0][2]; return z2 - z1; } ); // すべての影を描画 for(let shadow of shadowList) { shadow.draw(); } // すべての三角錐を描画 for(let sankaku of sankakuList) { sankaku.draw(); } } } //中途半端な衝突判定 /* function hitThreeCircle() { background(220); let x0 = 120; let y0 = 180; let r0 = 60; let x1 = mouseX; let y1 = mouseY; let r1 = 120; let d = sqrt((x1-x0)**2+(y1-y0)**2); let hit = d < r0+r1; noFill(); strokeWeight(4); circle(x0,y0,2*r0); circle(x1,y1,2*r1); stroke(0); textSize(32); text('d = '+nfc(d,2),30,420); } */ let game; function setup() { createCanvas(640,640); frameRate(15); game = new Game(); } function draw() { game.proc(); }