ファミコンROM作ってみた:開発編(キャラクター制御コード）

#ゲーム内容
作成ROM
[ファミコンROM作ってみた](https://qiita.com/tmori/items/a130fb0364dc5f757d67)

上記リンクにあるゲームのコードになります。

コード全体のイメージとしては下記の記事をご参照ください。
[ファミコンROM作ってみた:開発編(コード設計）](https://qiita.com/tmori/items/d49fd2b261fcab86f0a4)

コード内で利用している共通ライブラリについては下記の記事をご参照ください。
[ファミコンROM作ってみた:開発編(共通関数ライブラリ）](https://qiita.com/tmori/items/bc0cebefe64959337428)

コード内で利用しているプロダクト用関数ライブラリについては下記の記事をご参照ください。
[ファミコンROM作ってみた:開発編(プロダクト用関数ライブラリ）](https://qiita.com/tmori/items/ad667c6dd8cd2c27f80b)

メイン関数とフローのコードは下記の記事をご参照ください
[ファミコンROM作ってみた:開発編(mainとフローの処理コード）](https://qiita.com/tmori/items/ed9f4d85fb7fcc99ccae)

##参考・出典
日経BP発行「日経ソフトウエア」2021年3月号の特集記事「ファミコンで動くゲームを作ろう 第3部 オリジナルのゲームを完成させる」（著者：松原拓也氏）

※上記記事に掲載されたコードや内容を参考にしました。掲載にあたっては、著者および編集部の承諾をいただきました。

##各キャラクターの処理コードについて
以下のスプライトの制御コードになります。
・プレイヤー
・敵
・アイテム 
・鞭

各キャラクターの処理コードは主に下記の関数に分けて処理されています。
・init関数：フローの最初で呼ばれています。
・start関数：スプライト追加時に独自の引数を元に各データの初期設定が行われています。
・実行関数：スプライト表示時のループ時に呼び出される処理が記載されています。
 

##プレイヤー用コード　src/gm_player.c,h
プレイヤー用の操作は弾が打てるバルーンファイトの感触を目標にざっくり記載。
地面移動の際の横の移動量＋１が思いのほか大きくデメリットに感じない状態でしたのでプレイヤー用の位置データを別途short型でもって1/2してあります。
また、press_btn変数にボタンを押したときだけフラグが立つように格納
最後の方でジャンプボタンで羽が動くようにアニメーション処理が記載されています。

```gm_player.h
#ifndef __GM_PLAYER__H__
#define __GM_PLAYER__H__


typedef struct PlayerData
{
	signed char jump;
	signed char wing_cnt;

}PlayerData;

void player_init();
signed char player_start(unsigned char x, unsigned char y);
signed char player(signed char spr_no, unsigned char x, unsigned char y);


extern PlayerData g_player_data;
extern signed char g_player_spr_id;

#endif
```
```gm_player.c

#include "global.h"
#include "gm_bullet.h"

#include "gm_player.h"


PlayerData g_player_data;

signed char g_player_spr_id = -1;

void player_init()
{

}

signed char player_start(unsigned char x, unsigned char y) {
	signed char spr_id;
	
	spr_id = AddSpriteSet4(SPRITE_CHAR_TOP, x, y, 0);
	g_spr_idtbl[spr_id].pos_xy = x<<1;
	g_spr_idtbl[spr_id].func = player;
	return spr_id;
}

signed char player(signed char spr_no, unsigned char x, unsigned char y) {
	unsigned char btn, press_btn;
	signed short pos_y = y;
	signed char ofs_y;

	btn = g_now_pad1;
	press_btn = btn ^ (g_pre_pad1 & btn);

	if (((btn & BUTTON_LEFT) != 0) && (x > 8)) {
		if (pos_y >= 144) {
			//地面
			g_spr_idtbl[spr_no].pos_xy -= 1;
		}
		else {
			g_spr_idtbl[spr_no].pos_xy -= 4;
		}
	}

	if (((btn & BUTTON_RIGHT) != 0) && (x < 228)) {
		if (pos_y >= 144) {
			//地面
			g_spr_idtbl[spr_no].pos_xy += 1;
		}
		else {
			g_spr_idtbl[spr_no].pos_xy += 4;
		}
	}

	if (g_spr_idtbl[spr_no].pos_xy <= 8 * 2) {
		g_spr_idtbl[spr_no].pos_xy = 8 * 2;
	}
	if (g_spr_idtbl[spr_no].pos_xy >= 228 * 2) {
		g_spr_idtbl[spr_no].pos_xy = 228 * 2;
	}
	x = g_spr_idtbl[spr_no].pos_xy>>1;

	//if (((press_btn & BUTTON_UP) != 0)) { g_player_data.jump -= 8; }
	if (((press_btn & BUTTON_A) != 0)) {
		g_player_data.wing_cnt = 8;
		g_player_data.jump -= 16;
	}

	if (g_BulletData.bullet_anim_cnt == -1) {
		if ((press_btn & BUTTON_B) != 0) {
			 //鞭
			 g_whip_spr_id = bullet_start(x,y);

		}
	}

	if (g_player_data.jump < -16) {
		g_player_data.jump = -16;
	}
	if (g_player_data.jump < 24) {
		g_player_data.jump++;
	}

	ofs_y = 0;
	if ((btn & BUTTON_UP) != 0) { ofs_y -= 1; }
	if ((btn & BUTTON_DOWN) != 0) { ofs_y += 1; }

	pos_y = pos_y + (g_player_data.jump >> 2) + ofs_y;
	if (pos_y < 8) {
		pos_y = 8;
		if (g_player_data.jump < 0) {
			g_player_data.jump = 0;
		}
	}
	if (pos_y > 144) {
		pos_y = 144;
		g_player_data.jump = 0;
	}
	sp_ofs4(spr_no, x, (unsigned char)pos_y);
	if (g_player_data.wing_cnt >= 0) {
		if (g_player_data.wing_cnt == 8 || g_player_data.wing_cnt == 0) {
			sp_anime4(spr_no, SPRITE_NO(SPRITE_CHAR_TOP));
		}
		else {
			sp_anime4(spr_no, SPRITE_NO(SPRITE_CHAR_TOP) + 4);
		}
		g_player_data.wing_cnt--;
	}

	return 4;
}
```

##エネミー用コード　src/gm_monster.c,h
突撃してくるエネミーのコードです。
移動スピードはスタート関数の呼び出し時に設定。
スピードデータの保持にはスプライトテーブル構造体内のパラメータを利用しています。
一番左まで移動したらスプライトは削除。
ヒットチェックはプレイヤーと鞭でチェックして、プレイヤーに当たったらゲームオーバー処理へ、鞭に当たったらアイテムを発生させています。


```gm_monster.h
#ifndef __GM_MONSTER__H__
#define __GM_MONSTER__H__

void monster_init();
void monster_start(unsigned char x, unsigned char y, signed char spd);
signed char monster(signed char spr_no,  unsigned char x, unsigned char y);

#endif
```
```gm_monster.c

#include "global.h"
#include "gm_player.h"
#include "gm_bullet.h"
#include "gm_coin.h"

#include "gm_monster.h"



void monster_init() {

}

void monster_start(unsigned char x,unsigned char y, signed char spd)
{
	char spr_id;
	spr_id = AddSpriteSet4(SPRITE_ENEMY_TOP, x, y, 0);
	g_spr_idtbl[spr_id].param1 = spd;
	g_spr_idtbl[spr_id].func = monster;

}

signed char monster(signed char spr_no,  unsigned char x, unsigned char y) {
	unsigned char spd;
	if (x <= 1) {
		SetRemoveSprite4(spr_no);
		return 0;
	}
	spd = (unsigned char)(g_spr_idtbl[spr_no].param1 & 0xff);
	x--;
	x -= (spd % 3);
	sp_ofs4(spr_no, x, y);

	if ((g_gameScore.gameover == 0)) {
		if ((sp_hitcheck(g_player_spr_id, x, y) == nTRUE)) {
			//当たった
			SetRemoveSprite4(g_player_spr_id);
			pulse1(250);
			g_gameScore.gameover = 1;
		}
		if (g_whip_spr_id != -1 && (sp_hitcheck(g_whip_spr_id, x, y) == nTRUE)) {
			//当たった
			SetRemoveSprite4(spr_no);
			pulse1(200);
			coin_start(x,y, spd);
		}

	}

	return 4;
}

```
##アイテム用コード　src/gm_coin.c,h
アイテム用のコードです。
スタート関数内でランダムで何のアイテムが出るか決定しています。
上に飛んでいくスピードはエネミーから渡されたデータ量を最大に乱数で決定してますが、現在エネミーと同じスピードのパラメータを渡していますので、早い敵の方が上に上がっていく量が大きく猶予が得られるという仕様になっています。
地面よりも下に移動した際にスプライトは削除。
アイテムはプレイヤーとのヒットチェックを行い、ヒットした際に各アイテム種別事にグローバルのスコア情報に対して値を追加していきます。
ｙ座標はマイナス値になって下から出てこないようにクランプ処理を行っていますが、ｙ座標は内部的にスプライトテーブルのshort型のパラメータを元にしているため、画面外で移動しているように見せています。

```gm_coin.h
#ifndef __GM_COIN__H__
#define __GM_COIN__H__

void coin_init();
void coin_start(unsigned char x, unsigned char y, unsigned char spd);
signed char coin(signed char spr_no, unsigned char x, unsigned char y);

#endif
```
```gm_coin.c
#include "global.h"

#include "gm_coin.h"

const unsigned short  g_item_tbl[] = {
	SPRITE_ITEM_TOP,
	SPRITE_ITEM_TOP + 1,
	SPRITE_PUDDING_TOP
};

void coin_init()
{
}

void coin_start(unsigned char x,unsigned char y,unsigned char spd) {
	char spr_id;
	spr_id = AddSpriteSet1(g_item_tbl[(random() % 3)], x, y, 0);	//コイン

	g_spr_idtbl[spr_id].param1 = (random() % spd) * 2 + 8;
	g_spr_idtbl[spr_id].pos_xy = y;
	g_spr_idtbl[spr_id].func = coin;
}


signed char coin(signed char spr_no, unsigned char x, unsigned char y)
{
	if (x <= 1) {
		SetRemoveSprite1(spr_no);
		return 0;
	}

	x--;
	g_spr_idtbl[spr_no].pos_xy -= g_spr_idtbl[spr_no].param1 >> 2;
	g_spr_idtbl[spr_no].param1--;
	if (g_spr_idtbl[spr_no].param1 < -32) {
		g_spr_idtbl[spr_no].param1 = -32;
	}

	if (y > 8 * 21) {
		SetRemoveSprite1(spr_no);
		return 1;
	}

	if ((g_gameScore.gameover == 0) && (sp_hitcheck(SPRITE_NO(SPRITE_CHAR_TOP), x, y) == nTRUE)) {
		pulse1(500);
		switch (sp_gettile(spr_no)) {
		case SPRITE_NO(SPRITE_ITEM_TOP):
			g_gameScore.score += 1;
			break;
		case SPRITE_NO(SPRITE_ITEM_TOP + 1):
			g_gameScore.score += 10;
			break;
		case SPRITE_NO(SPRITE_PUDDING_TOP):
			g_gameScore.pddiing_num += 1;
			break;
		default:
			g_gameScore.score++;
			break;
		}
		SetRemoveSprite1(spr_no);
		return 1;
	}
	if (g_spr_idtbl[spr_no].pos_xy < 0) {
		y = 0;
	}
	else {
		y = g_spr_idtbl[spr_no].pos_xy;
	}
	sp_ofs1(spr_no, x, y);
	return 1;
}
```
##バレット用コード　src/gm_bullet.c,h
鞭の処理コードです。
投げて戻ってくるブーメラン状態の軌跡を描くように計算。
記載後にmath関数も利用できるのを知ったので、どっかで書き換える可能性もありますが、現状は2次関数のカーブを使って位置情報を処理してあります。
なお、移動量がマイナス（鞭が戻ってきている挙動中）のフェーズでプレイヤーのｙ位置に補正するように対応しています。
鞭のアニメーションパターンは４つ、1fr事にパターン変更しています。

```gm_bullet.h
#ifndef __GM_BULLET__H__
#define __GM_BULLET__H__

typedef struct BulletData
{
	signed short f_bullet_y;
	signed short f_bullet_x;
	signed short f_bullet_st_y;
	signed short f_bullet_st_x;
	signed short f_bullet_cur;
	signed char bullet_anim_cnt;
}BulletData;


void bullet_init();
signed char bullet_start(unsigned char x, unsigned char y);
signed char bullet(signed char spr_no, unsigned char x, unsigned char y);

extern BulletData g_BulletData;;
extern signed char g_whip_spr_id;

#endif
```
```gm_bullet.c

#include "global.h"
#include "gm_player.h"

#include "gm_bullet.h"


BulletData g_BulletData;

signed char g_whip_spr_id = -1;

void bullet_init()
{
	bullet_start(0,0);
}
signed char bullet_start(unsigned char x,unsigned char y) {
	signed char spr_id;
	spr_id = AddSpriteSet1(SPRITE_BULLET_TOP, x, y, 0);
	g_BulletData.f_bullet_st_x = x;
	g_BulletData.f_bullet_st_y = y;
	g_BulletData.f_bullet_y = 0;
	g_BulletData.f_bullet_x = 0;
	g_BulletData.f_bullet_cur = 0;
	g_BulletData.bullet_anim_cnt = -1;

	g_spr_idtbl[spr_id].func = bullet;
	return spr_id;
}

signed char bullet(signed char spr_no, unsigned char x, unsigned char y)
{
	signed short move_data,move_x_diff;
	signed short count_over = 8;
	BulletData *dt = &g_BulletData;

	dt->f_bullet_cur += 1;


	if (dt->f_bullet_cur < count_over) {
		//スタート1/4
		move_data = dt->f_bullet_cur;
		dt->f_bullet_x = dt->f_bullet_st_x + (move_data*move_data);
	}
	else if (dt->f_bullet_cur < count_over * 3) {
		//1/4～3/4
		move_data = (count_over * 2) - dt->f_bullet_cur;
		dt->f_bullet_x = dt->f_bullet_st_x + (count_over*count_over) * 2 - (move_data*move_data);
	}
	else if (dt->f_bullet_cur < count_over * 4) {
		//3/4～終了
		move_data = -(count_over * 4) + dt->f_bullet_cur;
		dt->f_bullet_x = dt->f_bullet_st_x + (move_data*move_data);
	}
	else {
		//終了
		SetRemoveSprite1(spr_no);
		dt->bullet_anim_cnt = -1;
		g_whip_spr_id = -1;
		return 1;
	}

	dt->f_bullet_y = dt->f_bullet_st_y + move_data;
	
	if (move_data < 0 ){
		//Y補正（※プレイヤー位置に追従）
		move_x_diff = (dt->f_bullet_x - dt->f_bullet_st_x) >> 4;
		if (move_x_diff > 0) {
			dt->f_bullet_y += (sp_gety(g_player_spr_id) - dt->f_bullet_y) / move_x_diff;
		}
		else {
			dt->f_bullet_y += sp_gety(g_player_spr_id) - dt->f_bullet_y;
		}
	}

	//オーバーフローでループしないように位置をクランプ
	if (dt->f_bullet_x < 0) {
		x = 0;
	}
	else if (dt->f_bullet_x < OUTSIDE_MAX){
		x = (unsigned char)(dt->f_bullet_x);
	}
	else {
		x = OUTSIDE_MAX;
	}
	if (dt->f_bullet_y < 0) {
		y = 0;
	}
	else if (dt->f_bullet_y < OUTSIDE) {
		y = (unsigned char)(dt->f_bullet_y);
	}
	else {
		y = OUTSIDE;
	}

	sp_ofs1(spr_no, x, y);

	dt->bullet_anim_cnt++;
	if (dt->bullet_anim_cnt >= 4 + 4 * 1) {
		dt->bullet_anim_cnt = 4;
	}
	sp_anime1(spr_no, SPRITE_NO(SPRITE_BULLET_TOP + ((dt->bullet_anim_cnt / 1) & 3)));

	return 1;
}

```
#関連記事一覧
[ファミコンROM作ってみた](https://qiita.com/tmori/items/a130fb0364dc5f757d67)
[ファミコンROM作ってみた:開発編(画像コンバーター）](https://qiita.com/tmori/items/e952a9d5e150e60af393)
[ファミコンROM作ってみた:開発編(環境）](https://qiita.com/tmori/items/14e250c52d362a403cbe)
[ファミコンROM作ってみた:開発編(ビルド）](https://qiita.com/tmori/items/ab1f163667878a7c9bf5)
[ファミコンROM作ってみた:開発編(コード設計）](https://qiita.com/tmori/items/d49fd2b261fcab86f0a4)
[ファミコンROM作ってみた:開発編(共通関数ライブラリ）](https://qiita.com/tmori/items/bc0cebefe64959337428)
[ファミコンROM作ってみた:開発編(プロダクト用関数ライブラリ）](https://qiita.com/tmori/items/ad667c6dd8cd2c27f80b)
[ファミコンROM作ってみた:開発編(mainとフローの処理コード）](https://qiita.com/tmori/items/ed9f4d85fb7fcc99ccae)
[ファミコンROM作ってみた:開発編(キャラクター制御コード）](https://qiita.com/tmori/items/ddf96075f05d0a1f710d)