ひとまず完成
まだまだ整理したいところがあるけれど、終わりが見えてきません。前回までのコード整理でひとまずの完成とします。実際の実行例を以下に示します。
全コード
ここまで作ってきた全コードを以下に示します。
/**
* 四角形を表すクラス
*/
class Rect {
/**
* 四角形の中心のX座標
*/
public X: number;
/**
* 四角形の中心のY座標
*/
public Y: number;
/**
* 四角形の横幅
*/
public Width: number;
/**
* 四角形の高さ
*/
public Height: number;
/**
* コンストラクタ
* 点(X1,Y1)と点(X2,Y2)を対角線上の頂点とする四角形を作る。
* @param x1 1つ目の頂点のX座標
* @param y1 1つ目の頂点のY座標
* @param x2 2つ目の頂点のX座標
* @param y2 2つ目の頂点のY座標
*/
constructor(x1: number, y1: number, x2: number, y2: number) {
this.X = (x1 + x2) / 2;
this.Y = (y1 + y2) / 2;
this.Width = Math.abs(x2 - x1);
this.Height = Math.abs(y2 - y1);
}
/**
* 四角形の左辺のX座標
*/
public get Left() { return this.X - this.Width / 2; }
/**
* 四角形の右辺のX座標
*/
public get Right() { return this.X + this.Width / 2; }
/**
* 四角形の上辺のY座標
*/
public get Top() { return this.Y - this.Height / 2; }
/**
* 四角形の下辺のY座標
*/
public get Bottom() { return this.Y + this.Height / 2; }
}
/**
* 円を表すクラス
*/
class Circle {
/**
* 円の中心のX座標
*/
public X: number;
/**
* 円の中心のY座標
*/
public Y: number;
/**
* 円の半径
*/
public Radius: number;
/**
* コンストラクタ
* 点(x,y)を中心とする半径Rの円を作る。
* @param x 中心のX座標
* @param y 中心のY座標
* @param r 中心の半径
*/
constructor(x: number, y: number, r: number) {
this.X = x;
this.Y = y;
this.Radius = Math.abs(r);
}
/**
* 円の最も元も左のX座標
*/
public get Left() { return this.X - this.Radius; }
/**
* 円の最も右のX座標
*/
public get Right() { return this.X + this.Radius; }
/**
* 円の最も上のY座標
*/
public get Top() { return this.Y - this.Radius; }
/**
* 円の最も下のY座標
*/
public get Bottom() { return this.Y + this.Radius; }
}
/**
* ゲームの部品を表すインターフェイス
*/
interface IGamePart {
/**
* ゲームフレームが更新されたときに、部品の状態を更新するメソッド
*/
Update(): void;
/**
* Updateの後で、部品の状態をプレイヤーに見せるための表示するメソッド
*/
Show(): void;
}
/**
* ゲームループとIGamePartのオブジェクトを管理するクラス
*/
class GameLoop {
/**
* 1秒毎のゲームフレーム数 Frame Per Second
*/
public set FPS(value: number) {
if (value > 0) {
this._FPS = value;
}
else {
console.log("FPSは0より大きな数値にしてください。");
}
}
public get FPS() { return this._FPS; }
/**
* 1秒毎のゲームフレーム数 Frame Per Second
*/
private _FPS: number;
/**
* ゲーム部品のリスト
*/
private _partlist: IGamePart[] = [];
/**
* コンストラクタ
*/
constructor() {
this._FPS = 30;
}
/**
* ゲームループ処理開始
*/
public Start() {
window.requestAnimationFrame(timestamp => this._Loop(timestamp));
}
/**
* フレームカウンタ(ゲーム内の時刻1カウンタ)
* GameLoop()が最初にコールされたときを0とし、
* 1/FPS秒を1フレームとしてカウントする。
*/
public get Frame() { return this._FrameCounter; }
/**
* フレームカウンタ(ゲーム内の時刻1カウンタ)
* GameLoop()が最初にコールされたときを0とし、
* 1/FPS秒を1フレームとしてカウントする。
*/
private _FrameCounter: number | undefined;
/**
* _Loop()が最初にコールされたときのtimestampを保持する
*/
private _LoopStartTime = 0;
/**
* ゲームループ処理本体
* window.requestAnimationFrame()のコールバック
* @param timestamp window.requestAnimationFrame()から渡されるタイムスタンプ
*/
private _Loop(timestamp: number) {
// 初めてコールされたときに、_LoopStartTimeと_FrameCounterを初期化
if (this._FrameCounter === undefined) {
this._FrameCounter = 0;
this._LoopStartTime = timestamp;
}
// ゲームループ開始時と現在のtimestampの差から、フレーム数を計算
let frame = Math.floor((timestamp - this._LoopStartTime) * this._FPS / 1000);
if (frame > this._FrameCounter) {
// フレームカウンタを更新
this._FrameCounter = frame;
// フレーム数が更新されたので、ゲームの状態を更新する。
this._Update();
// フレーム数が更新されたので、ゲームの状態を表示する。
this._Show();
}
// ゲームの状況更新、再描画のために、requestAnimationFrame()に再度実行する。
window.requestAnimationFrame(timestamp => this._Loop(timestamp));
}
/**
* ゲームの状態を更新する。
*/
private _Update() {
// ゲーム部品を更新する。
this._partlist.forEach(part => part.Update());
}
/**
* ゲームの状態を表示する。
*/
private _Show() {
// ゲーム部品を表示する。
this._partlist.forEach(part => part.Show());
}
/**
* ゲーム部品を登録する
* @param part ゲーム部品
*/
public AddPart(part: IGamePart) {
this._partlist.push(part);
}
}
/**
* キャンバスに描画されるゲームの部品を表すインターフェイス
*/
interface IGameCanvasPart {
/**
* ゲームフレームが更新されたときに、部品の状態を更新するメソッド
*/
Update(): void;
/**
* Updateの後で、部品をCANVASに描画するメソッド
* @param context 描画するCANVASのCanvasRenderingContext2D
*/
DrawCanvas(context: CanvasRenderingContext2D): void;
}
/**
* ゲームパラメーター
*/
class GameParameter {
/**
* FPS(Frame per second)
*/
static readonly FPS = 20;
/**
* 球の移動計算で使う、重力加速度
*/
static readonly gravity = 0.25;
/**
* 球が移動できる範囲
*/
static readonly Area = new Rect(10, 10, 310, 310);
};
/**
* 球を表すクラス
*/
class Ball implements IGameCanvasPart {
/**
* 球本体の円
*/
private _body: Circle;
/**
* X座標の速度
*/
private _VX = 0;
/**
* Y座標の速度
*/
private _VY = 0;
/**
* コンストラクタ
*/
constructor(private _game: GameMng) {
// 球のサイズ10で宣言
this._body = new Circle(0, 0, 10);
}
/**
* ゲーム開始時の初期化処理
*/
public Init() {
// フレームカウンターをランダム値として使う。
let random = this._game.Random(1, 18); // 1~18までのランダム値
// X座標:初期値はフレームカウンタに応じて、GameParameter.Areaの横幅の0.1~0.9の位置にランダムに変化する。
let random2 = 0.1 * (1 + (random % 9)); // randomから0.1~0.9まで0.1刻みの値を作る。
this._body.X = GameParameter.Area.Left + GameParameter.Area.Width * random2;
// Y座標:初期値はGameParameter.Areaの上辺から球の半径だけ下
this._body.Y = GameParameter.Area.Top + this._body.Radius;
// X軸速度:初期値はランダム値が奇数なら-5、偶数なら5に変化する。
this._VX = (random & 1) ? 5 : -5;
// Y軸速度:初期値は0
this._VY = 0;
}
/**
* 状態更新
*/
public Update() {
if (this._game.Status == "game") {
let N = 10;
for (let i = 0; i < N; i++) {
// 速度を更新する。(下向きに加速する)
this._VY += GameParameter.gravity / N;
// 衝突を考慮せずに、座標と速度を更新する。
this._body.X += this._VX / N;
this._body.Y += this._VY / N;
// ラケットとの衝突処理
this._CollisionRachket();
// 左右の壁との衝突処理
if (this._VX < 0 && this._body.Left < GameParameter.Area.Left) {
// X座標速度を反転する。
this._VX = -this._VX;
// 得点を加算
this._game.Score++;
}
else if (this._VX > 0 && this._body.Right > GameParameter.Area.Right) {
// X座標速度を反転する。
this._VX = -this._VX;
// 得点を加算
this._game.Score++;
}
// 天井との衝突処理
if (this._VY < 0 && this._body.Top < GameParameter.Area.Top) {
// Y座標速度を半分にして反転する。
this._VY *= -0.5;
// 得点を加算
this._game.Score += 3;
}
// 床との衝突処理
if (this._body.Bottom > GameParameter.Area.Bottom) {
this._game.Status = "gameover";
}
}
}
}
/**
* ラケットとの衝突処理
*/
private _CollisionRachket() {
let racket_rect = this._game.Racket.Rect;
// 球がラケットに当たるとき、以下の条件を満たす。
// - 球が下向きに移動している
// - 球の最も下(_body.Bottom)がラケットの上面(racket_rect.Top)より下になる
// - 球のX座標がラケットの左右の間にある。
if (this._VY > 0 && racket_rect.Top < this._body.Bottom) {
if (racket_rect.Left <= this._body.X && this._body.X <= racket_rect.Right) {
// ラケットの幅を5つに分割し、球が当たった位置に応じて跳ね返り方を変える。
// 1.ラケットの左側10%の範囲に当たったとき
// Y軸の速度を0.8倍にして反転する。
// X軸の速度を左向きに5~10にする。
// 2.ラケットの右側10%の範囲に当たったとき
// Y軸の速度を0.8倍にして反転する。
// X軸の速度を右向きに5~10にする。
// 3.ラケットの中央40%の範囲に当たったとき
// Y軸の速度を1.2倍にして反転する。
// X軸の速度が5を超えるときは、1つ減らす。
// 4.上記以外
// Y軸の速度を0.9倍にして反転する。
let dx = (this._body.X - racket_rect.Left) / racket_rect.Width;
if (dx < 0.1) { // 左側10%
this._VY *= -0.8;
this._VX = this._game.Random(-10, -5); // 左向きに5~10
}
else if (0.9 < dx) { // 右側10%
this._VY *= -0.8;
this._VX = this._game.Random(5, 10); // 右向きに5~10
}
else if (0.3 <= dx && dx <= 0.7) { // 中央40%
this._VY *= -1.2;
if (this._VX > 5) { this._VX--; }
else if (this._VX < -5) { this._VX++; }
}
else {
this._VY *= -0.9;
}
}
}
}
/**
* CANVASへ描画する
* @param context
*/
public DrawCanvas(context: CanvasRenderingContext2D) {
context.fillStyle = "yellow";
context.beginPath();
context.arc(this._body.X, this._body.Y, this._body.Radius, 0, 2 * Math.PI);
context.closePath();
context.fill();
}
}
/**
* ラケットを表すクラス
*/
class Racket implements IGameCanvasPart {
/**
* ラケット本体の四角
*/
private _body: Rect;
/**
* ラケット本体の四角
*/
public get Rect() { return this._body; }
/**
* コンストラクタ
*/
constructor(private _game: GameMng) {
this._body = new Rect(0, 0, 40, 10);
}
/**
* ゲーム開始時の初期化処理
*/
public Init() {
// X座標:初期値はGameParameter.Areaの横幅の真ん中
this._body.X = GameParameter.Area.X;
// Y座標:初期値はGameParameter.Areaの下辺からラケットの縦幅の半分だけ上
this._body.Y = GameParameter.Area.Bottom - this._body.Height / 2
// 幅は40
this._body.Width = 40;
}
/**
* 状態更新
*/
public Update() {
let left = GameParameter.Area.Left + this._body.Width / 2;
let right = GameParameter.Area.Right - this._body.Width / 2;
// ラケットの移動先と現在値との距離
let distance = this._game.Canvas.Mouse.X - this._body.X;
// 距離に応じて、X軸の変化速度を変更する。
let vx = distance / 5;
// ラケットを移動する。
let x = this._body.X + vx;
if (x < left) {
x = left;
}
else if (x > right) {
x = right;
}
this._body.X = x;
// ラケットの幅をScoreに応じて短くする。
let width = 40 - 2 * Math.floor(this._game.Score / 20);
if (width < 10) {
width = 10;
}
this._body.Width = width;
}
/**
* CANVASへ描画する
* @param context
*/
public DrawCanvas(context: CanvasRenderingContext2D) {
context.fillStyle = "yellowgreen";
context.fillRect(this._body.Left, this._body.Top, this._body.Width, this._body.Height);
}
}
/**
* \<div id="xxxx">や\<button id="yyyy">のような、
* id付のHTML要素を管理するためのクラスのテンプレート
*/
abstract class ElementMng<T extends HTMLElement> implements IGamePart {
/**
* HTML要素のID名
*/
protected _id;
/**
* _idに対するHTML要素
*/
protected _element: T | undefined;
/**
* _elementを使った処理を必須のときに、
* _elementがセットされるまで待つためのPromise。
* 次のように使う
*
* this._AsyncElement.then(element => { elementを使った処理})
*/
protected _AsyncElement: Promise<T>;
/**
* コンストラクタ
*
* 本クラスは継承して使うことを前提としているので、継承時の注意
*
* 継承で、\<div id="SCORE">に対する管理クラスを作る時には、
* そのコンストラクタ内で
*
* super("SCORE",HTMLDivElement);
*
* と記載すること。
* @param id ID名
* @param typecheck 型チェックするためにHTMLElementの派生クラスを指定する
*/
constructor(id: string, typecheck: new (...args: any) => T) {
this._id = id;
this._AsyncElement = new Promise<T>(resolve => {
document.addEventListener("DOMContentLoaded", () => {
let element = document.getElementById(id);
if (element instanceof typecheck) {
this._element = element;
resolve(element);
}
else {
let typeName = this._getTypeName(typecheck);
console.log(`id="${id}"の${typeName}要素がありません。`)
}
});
});
}
/**
* typecheckに対応するクラス名が HTML*Element に一致するときに、*の部分の文字列を返す。
* @param typecheck HTMLElementの派生クラスのコンストラクタ
* @returns 文字列
*/
private _getTypeName(typecheck: new (...args: any) => T) {
let match = typecheck.toString().match(/HTML(.*)Element/);
if (match) {
return match[1];
}
else {
return "";
}
}
public abstract Update(): void;
public abstract Show(): void;
}
/**
* ゲームCANVAS(\<canvas id="game">)を管理するクラス
*/
class GameCanvasMng extends ElementMng<HTMLCanvasElement> {
/**
* マウスの情報
*/
public get Mouse() {
return {
/**
* CANVAS上のX座標
*/
X: this._mouseCanvasX,
/**
* CANVAS上のY座標
*/
Y: this._mouseCanvasY,
}
}
/**
* マウスのCANVAS上のX座標
*/
private _mouseCanvasX = 0;
/**
* マウスのCANVAS上のY座標
*/
private _mouseCanvasY = 0;
/**
* コンストラクタ
*/
constructor() {
super("game", HTMLCanvasElement);
this._AsyncElement.then(element => {
element.addEventListener("mousemove", (ev) => {
let p = this._MouseToCanvas(ev);
this._mouseCanvasX = p.X;
this._mouseCanvasY = p.Y;
});
});
}
/**
* マウス座標をevから取得し、CANVAS座標に変換する
* @param ev
* @returns CANVAS座標
*/
private _MouseToCanvas(ev: MouseEvent) {
// このメソッドは_elementに対するマウスイベント発生時にコールされるので、
// _elementは必ず存在する。
let canvas = this._element!;
let style = window.getComputedStyle(canvas);
let paddingLeft = Number(style.paddingLeft.replace("px", ""));
let paddingTop = Number(style.paddingTop.replace("px", ""));
let paddingRight = Number(style.paddingRight.replace("px", ""));
let paddingBottom = Number(style.paddingBottom.replace("px", ""));
let styleWidth = canvas.clientWidth - paddingLeft - paddingRight;
let styleHeight = canvas.clientHeight - paddingTop - paddingBottom;
let x = (ev.offsetX - paddingLeft) * canvas.width / styleWidth;
let y = (ev.offsetY - paddingTop) * canvas.height / styleHeight;
return { X: x, Y: y }
}
/**
* CANVASの部品を更新する。
*/
public Update() {
// ゲーム部品を更新する。
this._partlist.forEach(part => part.Update());
}
/**
* CANVASの部品を描画する。
*/
public Show() {
if (this._element) {
let width = this._element.width;
let height = this._element.height;
let context = this._element.getContext("2d");
if (context && width > 0 && height > 0) {
let _context = context;
// ゲーム部品を表示する。
this._partlist.forEach(part => part.DrawCanvas(_context));
}
}
}
/**
* CANVAS部品のリスト
*/
protected _partlist: IGameCanvasPart[] = [];
/**
* CANVAS部品を登録する
* @param part CANVAS部品
*/
public AddPart(part: IGameCanvasPart) {
this._partlist.push(part);
}
}
/**
* メッセージボックス(\<div id="message">)を管理するクラス
*/
class MessageMng extends ElementMng<HTMLDivElement> {
/**
* innerTextに表示するテキスト
*/
private _text = "";
/**
* コンストラクタ
*/
constructor(private _game: GameMng) {
super("message", HTMLDivElement);
}
/**
* メッセージボックスを更新する。
*/
public Update() {
switch (this._game.Status) {
case "start":
this._text = "ボタンを押すとゲームを開始します。";
break;
case "game":
this._text = "スコア:" + this._game.Score;
break;
case "stop":
this._text = "ゲームポーズ中\nボタンを押すと再開します。";
break;
case "gameover":
this._text = "ゲームオーバー\n" +
"あなたのスコアは" + this._game.Score + "でした。\n" +
"ボタンを押すと次のゲームを開始します。";
break;
}
}
/**
* メッセージボックスを表示する。
*/
public Show() {
if (this._element) {
this._element.innerText = this._text;
}
}
}
/**
* ボタン1(\<button id="button1">)を管理するクラス
*/
class Button1Mng extends ElementMng<HTMLButtonElement> {
/**
* innerTextに表示するテキスト
*/
private _text = "";
/**
* コンストラクタ
*/
constructor(private _game: GameMng) {
super("button1", HTMLButtonElement);
this._AsyncElement.then(element => {
element.addEventListener("click", e => { this._game.Button1_Clicked() });
});
}
/**
* メッセージボックスを更新する。
*/
public Update() {
switch (this._game.Status) {
case "start":
this._text = "スタート";
break;
case "game":
this._text = "ストップ";
break;
case "stop":
this._text = "スタート";
break;
case "gameover":
this._text = "NEXTゲーム";
break;
}
}
/**
* メッセージボックスを表示する。
*/
public Show() {
if (this._element) {
this._element.innerText = this._text;
}
}
}
/**
* ゲームの状況を表すtype
* start - ゲーム開始
* stop - ゲーム停止中
* game - ゲーム実行中
* gameover - ゲームオーバー
*/
type GameStatus_t = 'start' | 'stop' | 'game' | 'gameover';
/**
* ゲーム全体を管理するクラス
*/
class GameMng extends GameLoop {
/**
* ゲームの状況
*/
public Status: GameStatus_t = "start";
/**
* ゲームのスコア(点数)
*/
public Score = 0;
/**
* 球
*/
public Ball = new Ball(this);
/**
* ラケット
*/
public Racket = new Racket(this);
/**
* id="game"のCANVAS
*/
public Canvas = new GameCanvasMng();
/**
* id="button1"のBUTTON
*/
public Button1 = new Button1Mng(this);
/**
* id="messgae"のDIV
*/
public Message = new MessageMng(this);
/**
* コンストラクタ
*/
constructor() {
super();
this.FPS = GameParameter.FPS;
// 部品を登録
this.AddPart(this.Canvas);
this.AddPart(this.Message);
this.AddPart(this.Button1);
// CANVAS部品を登録
this._CanvasAddParts();
// ゲーム状態の初期化
this.InitSituation();
}
/**
* Canvas部品を登録する
*/
private _CanvasAddParts() {
let background = new class implements IGameCanvasPart {
Update(): void { }
DrawCanvas(context: CanvasRenderingContext2D): void {
// 実際の描画処理
// 背景を黒に
context.fillStyle = "black";
context.fillRect(0, 0, context.canvas.width, context.canvas.height);
// GameParameter.Areaの範囲を白で描画
context.strokeStyle = "white";
let area = GameParameter.Area;
context.strokeRect(area.Left, area.Top, area.Width, area.Height);
}
};
this.Canvas.AddPart(background);
this.Canvas.AddPart(this.Racket);
this.Canvas.AddPart(this.Ball);
}
/**
* ゲームの状態を初期化する
*/
public InitSituation() {
// ボールの座標、速度を初期化する。
this.Ball.Init();
// ラケットの座標と幅を初期化する。
this.Racket.Init();
// ゲームスコアのクリア
this.Score = 0;
}
/**
* Button1がクリックされたときのイベントハンドラ
* ButtonMngの中で使用する。
*/
public Button1_Clicked() {
// ボタン1がクリックされたことで、ゲームの状態が変化する
switch (this.Status) {
case "game":
// 現在実行中 -> 停止に変化
this.Status = "stop";
break;
case "stop":
// 現在事項中 -> 実行に変化
this.Status = "game";
break;
case "start":
case "gameover":
// 現在開始 or ゲームオーバー -> 球やラケットなどの状態を初期化して、実行に変化
this.InitSituation();
this.Status = "game";
break;
}
}
/**
* Frameの値を使って、minとmaxの間の整数乱数値を得る。
* @param min
* @param max
* @returns
*/
public Random(min: number, max: number) {
let frame = this.Frame ?? 0;
if (max < min) {
console.log('error: max:{max} min:{min}')
}
let diff = max - min + 1;
return (frame % diff) + min;
}
}
const Game = new GameMng();
// ゲーム開始
Game.Start();
コメント