ライン

入門ワンチップテレビゲーム製作基礎

ライン

2014年7月21日〜

 PICマイコンを使ってたったワンチップで簡単にテレビゲームを製作できる環境が整いましたので、使い方の解説を書いていこうと思います。PIC32MXシリーズの書き込みができる環境と、C言語の基本的な知識は最低限必要となりますが、ビデオ信号の知識は全く必要ありません。
 (2015.11.21) 今後のシステム対応のため、全体にポートの設定値を変更しました。

ハードウェア

 これまで各種ゲームを作成した回路を使います。使用するマイコンはPIC32MX150F128BまたはPIC32MX250F128Bです。どちらでも動作するように作られています。ブレッドボードでも十分動作します。電源は写真のように乾電池2本でも大丈夫です。システムクロックの入力には、3.579545MHzの水晶発振子を使用してください。
基板写真

回路図

環境準備

 MPLAB IDEまたはMPLAB X IDE上で開発を行います。コンパイラはC32またはXC32となります。本記事ではMPLAB IDEとC32コンパイラで解説しますが、動作はMPLAB XとXC32でも問題ありません。ただし、XC32 v1.33以降には対応しておりません。
 マイコンのフラッシュへの書き込みにはPICkit3を使用していますが、その他の方法でも構いません。

プロジェクトの作成

 最初にプロジェクトを以下の要領で作成します。
1.プロジェクトを保存したいフォルダを作成
2.そのフォルダに次の3つのファイルをコピー
  composite32-high4.h
  lib_composite32-high.h
  lib_composite32-high.a
   ファイルのダウンロード

3.同じフォルダにメインプログラムとなるファイルを新規作成。ここではgame1.cとします。

4.MPLABでプロジェクトを作成
 MPLAB IDEを起動します。メニューのProjectのProject Wizardを起動し、マイコンやコンパイラの選択を行い、「Create New Project File」を選びます。プロジェクト名はgame1としました。その後、先ほどの4つのファイルをプロジェクトに追加してください。右図のようになれば完成です。

画面1

動作確認

 早速動作確認を行います。
 game1.cに以下のソースプログラムを入力します。
 こちらでダウンロードもできます。

					
#include <plib.h>
#include "composite32-high4.h"
#include "lib_composite32-high.h"

//外付けクリスタル with PLL (15倍)
#pragma config PMDL1WAY = OFF, IOL1WAY = OFF
#pragma config FPLLIDIV = DIV_1, FPLLMUL = MUL_15, FPLLODIV = DIV_1
#pragma config FNOSC = PRIPLL, FSOSCEN = OFF, POSCMOD = XT, OSCIOFNC = OFF
#pragma config FPBDIV = DIV_1, FWDTEN = OFF, JTAGEN = OFF, ICESEL = ICS_PGx1

void main(void){
	int i;

	//ポートの初期設定
	TRISA = 0x0010; // RA4は入力
	CNPUA = 0x0010; // RA4をプルアップ
	ANSELA = 0x0000; // 全てデジタル
	TRISB = KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// ボタン接続ポート入力設定
	CNPUBSET=KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// プルアップ設定
	ANSELB = 0x0000; // 全てデジタル
	LATACLR=2;// RA1=0(ボタンモード)

	init_composite(); // ビデオ出力システムの初期化
	for(i=0;i<=15;i++){
		printstr(0,i*10,i,-1,"HELLO PIC"); //文字列出力
	}
	while(1) asm("wait"); //停止
}
					
					

 ProjectのBuild Configrationを「Release」にして、「Build All」を実行します。もしエラーが出た場合はソースに間違いがないか、プロジェクトにファイルが追加されているかなどを確認してください。
 ビルドが完了すればいよいよ書き込みです。PICkit 3を基板に接続してください。ProgrammerのSelect ProgrammerでPICkit 3を選択します。そのままでは、電源が入らず認識されませんので、ボード側の電源を入れるか、ProgrammerのSettings →Power →Power target circuit from PICkit 3 でPICkit 3から3.3Vの電源を供給します。

画面2

 PICkit 3の認識が完了すれば、「Program」アイコンで書き込みを行います。書き込みが成功すれば、テレビのビデオ入力端子に接続し、電源を入れてください。写真のような画面が出れば成功です。どうですか?簡単でしょう?テレビに表示している間、PICは実は大変な仕事をしています。でもそんなことは意識しなくてもよいのです。基本的な機能はライブラリ化されているので、それを呼び出すだけでテレビ出力を実現できるのです。

実験写真

ライブラリ関数一覧

 システム仕様は以下の通りとなります。

解像度256ドット(横)×224ドット(縦)
色数16色同時表示(1677万色からパレットで設定。ただし実際の再現性は遥かに低い)

 ライブラリファイルは「lib_composite32-high.a」の1つにまとめましたが、インクルードファイルは2つに分けています。

(1)composite32-high4.h  映像出力システム関連

    void init_composite(void)
      システム初期化。画面消去して、テレビへの出力開始

    void stop_composite(void)
      画面出力停止

    void start_composite(void)
      画面出力再開。stop_composite()で停止したときの再開用

    void clearscreen(void)
      画面消去(カラーパレット番号0で塗りつぶし)

    void set_palette(unsigned char n , unsigned char b , unsigned char r , unsigned char g)
      カラーパレット設定
      n:設定するカラーパレット番号(0〜15) ※以降カラーパレット番号は単純に「カラー」と略
      r,g,b:赤、緑、青の強さ(各0〜255)
      初期値は0〜7までは順に黒、青、赤、紫、緑、水色、黄、白。8は0と同じ。9〜15は1〜7の明るさ半分

(2)lib_composite32-high.h  グラフィック描画関連ライブラリ

    void pset(int x , int y , unsigned int c)
      座標(x,y)の位置にカラーcで点を描画

    void line(int x1 , int y1 , int x2 , int y2 , unsigned int c)
      座標(x1,y1)から(x2,y2)にカラーcで線分を描画

    void hline(int x1 , int x2 , int y , unsigned int c)
      座標(x1,y)から(x2,y)への水平ラインをカラーcで高速描画

    void boxfill(int x1 , int y1 , int x2 , int y2 , unsigned int c)
      座標(x1,y1),(x2,y2)を対角線とするカラーcで塗られた長方形を描画

    void circle(int x0 , int y0 , unsigned int r , unsigned int c)
      座標(x0,y0)を中心に、半径r、カラーcの円を描画

    void circlefill(int x0 , int y0 , unsigned int r , unsigned int c)
      座標(x0,y0)を中心に、半径r、カラーcで塗られた円を描画

    void putfont(int x , int y , unsigned int c , int bc , unsigned char n)
      座標(x,y)にカラーcで8*8ドットのフォント表示
      bc:背景色(文字部分以外の塗りつぶし色)を指定。負数の場合背景色での塗りつぶしなし
      n:文字番号

    void printstr(int x , int y , unsigned int c , int bc , unsigned char *s)
      座標(x,y)にカラーcで文字列sを表示、bc:背景色(負数の場合背景色指定なし)

    void printnum(int x , int y , unsigned char c , int bc , unsigned int n)
      座標(x,y)にカラーcで数値nを10進数表示、bc:背景色(負数の場合背景色指定なし)

    void printnum2(int x , int y , unsigned char c , int bc , unsigned int n , unsigned char e)
      座標(x,y)にカラーcで数値nをe桁で右揃えして10進数表示、bc:背景色(負数の場合背景色指定なし)

    void putbmpmn(int x , int y , char m , char n , const unsigned char bmp[ ])
      横m*縦nドットのキャラクターを座標(x,y)に表示
      サイズm*nの配列bmpに、単純にカラー番号を並べる
      ただし、カラーが0の部分は透明色として扱う(詳細は別途解説)

    void clrbmpmn(int x , int y , char m , char n)
      縦m*横nドットのキャラクター消去(カラー0で塗りつぶし)


サンプルプログラムと解説

 このライブラリを使ったサンプルプログラムを作りました。game1.cをこちらで書き換え、ビルドして書き込み、テレビにつないで確かめてみてください。ソースはこちらでもダウンロードできます。

実験写真

					
#include <plib.h>
#include "composite32-high4.h"
#include "lib_composite32-high.h"

//外付けクリスタル with PLL (15倍)
#pragma config PMDL1WAY = OFF, IOL1WAY = OFF
#pragma config FPLLIDIV = DIV_1, FPLLMUL = MUL_15, FPLLODIV = DIV_1
#pragma config FNOSC = PRIPLL, FSOSCEN = OFF, POSCMOD = XT, OSCIOFNC = OFF
#pragma config FPBDIV = DIV_1, FWDTEN = OFF, JTAGEN = OFF, ICESEL = ICS_PGx1

const unsigned char bmp[14*13]={
	0,0,0,0,0,2,2,2,2,0,0,0,0,0,
	0,0,0,2,2,2,2,2,2,2,2,0,0,0,
	0,0,2,2,2,2,2,2,2,2,2,2,0,0,
	0,2,2,2,7,7,2,2,2,2,7,7,2,0,
	0,2,2,7,7,7,7,2,2,7,7,7,7,0,
	0,2,2,7,7,8,8,2,2,7,7,8,8,0,
	2,2,2,7,7,8,8,2,2,7,7,8,8,2,
	2,2,2,2,7,7,2,2,2,2,7,7,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,0,2,2,2,0,0,2,2,2,0,2,2,
	2,0,0,0,2,2,0,0,2,2,0,0,0,2
};
void main(void){
	int i;

	//ポートの初期設定
	TRISA = 0x0010; // RA4は入力
	CNPUA = 0x0010; // RA4をプルアップ
	ANSELA = 0x0000; // 全てデジタル
	TRISB = KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// ボタン接続ポート入力設定
	CNPUBSET=KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// プルアップ設定
	ANSELB = 0x0000; // 全てデジタル
	LATACLR=2;// RA1=0(ボタンモード)

	init_composite(); // ビデオ出力システムの初期化

	//テストパターン
	for(i=1;i<=7;i++){
		boxfill(i*30,0,i*30+29,30,8-i);
	}

	//線分
	for(i=1;i<=7;i++){
		line(60,40,i*15,120,i);
	}

	//円
	for(i=1;i<=7;i++){
		circle(180,75,i*6,i);
	}

	//塗りつぶし円と数字(色見本)
	for(i=0;i<=7;i++){
		circlefill(i*30+15,150,15,i);
		printnum(i*30+8,146,7,-1,i);
	}
	for(i=0;i<=7;i++){
		circlefill(i*30+15,180,15,i+8);
		printnum(i*30+8,176,7,-1,i+8);
	}

	//キャラクター表示
	for(i=1;i<=10;i++){
		putbmpmn(i*20,200,14,13,bmp);
	}
	while(1) asm("wait"); //停止
}
					
					

 冒頭の#includeと#pragmaについては、決まり文句だと思って、必要のない限り変更しないでください。
 続く配列bmpの定義は、putbmpmn関数で表示する横14ドット、縦13ドットのキャラクターを表現しています。数値はカラーで、この通りの色の順に点が出力されます。実行結果の写真とよく見比べてみてください。ただし0だけは特別値で、黒で塗るのではなく何も描画しません。キャラクターが重なった時など、下のキャラクターを消さずに重ねることができます。黒はカラー8で代用しています。
 main関数冒頭ではI/Oポートの初期化を行っています。こちらも必要のない限り変更しないでください。
 次のinit_composite関数で、システムの初期化を行います。タイマー割り込みが開始され、映像信号出力が始まります。あとは、上述のライブラリ関数を使用したサンプルです。塗りつぶした円の中に数字が書かれている部分は、カラーパレットの初期設定値ですので参考にしてください。
 最後に無限ループでasm("wait")とありますが、これは待機時になるべく省電力にするための方法です。


キャラクターの移動

 次はキャラクターを移動させてみます。今回もgame1.cを下記のように書き換えてビルドし、実行させてください。非常に単純な内容で、キャラクターが左右に動きます。ソースはこちらでもダウンロードできます。実際に変更したのは、main関数のみです。

実験写真

左右に動きます

					
#include <plib.h>
#include "composite32-high4.h"
#include "lib_composite32-high.h"

//外付けクリスタル with PLL (15倍)
#pragma config PMDL1WAY = OFF, IOL1WAY = OFF
#pragma config FPLLIDIV = DIV_1, FPLLMUL = MUL_15, FPLLODIV = DIV_1
#pragma config FNOSC = PRIPLL, FSOSCEN = OFF, POSCMOD = XT, OSCIOFNC = OFF
#pragma config FPBDIV = DIV_1, FWDTEN = OFF, JTAGEN = OFF, ICESEL = ICS_PGx1

const unsigned char bmp[14*13]={
	0,0,0,0,0,2,2,2,2,0,0,0,0,0,
	0,0,0,2,2,2,2,2,2,2,2,0,0,0,
	0,0,2,2,2,2,2,2,2,2,2,2,0,0,
	0,2,2,2,7,7,2,2,2,2,7,7,2,0,
	0,2,2,7,7,7,7,2,2,7,7,7,7,0,
	0,2,2,7,7,8,8,2,2,7,7,8,8,0,
	2,2,2,7,7,8,8,2,2,7,7,8,8,2,
	2,2,2,2,7,7,2,2,2,2,7,7,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,0,2,2,2,0,0,2,2,2,0,2,2,
	2,0,0,0,2,2,0,0,2,2,0,0,0,2
};
void main(void){
	int x,dx;

	//ポートの初期設定
	TRISA = 0x0010; // RA4は入力
	CNPUA = 0x0010; // RA4をプルアップ
	ANSELA = 0x0000; // 全てデジタル
	TRISB = KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// ボタン接続ポート入力設定
	CNPUBSET=KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// プルアップ設定
	ANSELB = 0x0000; // 全てデジタル
	LATACLR=2;// RA1=0(ボタンモード)

	init_composite(); // ビデオ出力システムの初期化

	x=0; //横位置
	dx=1; //移動方向

	while(1){
		//キャラクター表示
		putbmpmn(x,100,14,13,bmp);

		//60分の1秒ウェイト
		drawcount=0;
		while(drawcount==0) asm("wait");

		//キャラクター消去
		clrbmpmn(x,100,14,13);

		//横方向移動
		x+=dx;

		//移動向き変更
		if(x==200) dx=-1;
		else if(x==0) dx=1;
	}
}
					
					

 一般的にコンピュータプログラムで、画面上の何かを移動させるためには、「表示」→「ウェイト」→「消去」→「移動」を繰り返します。表示と消去の間には必ずウェイトを入れないと、目に留まりません。
 これを本プログラムでは「putbmpmn」→「ウェイト」→「clrbmpmn」→「変数xの変更」で実現しています。
 ところで、ウェイト部分は特殊なものとなっています。ウェイトなんて適当に空ループを回せばいいと思われるかもしれません。動きがゆっくりな場合にはそれでも通用しますが、速い場合には問題があります。テレビへの表示は、本システムの場合、1秒間にほぼ60回行っています。1回当たり約16.7ミリ秒です。この16.7ミリ秒の内、約14.2ミリ秒の間は画面の上から下に向かってテレビに対して順番に映像信号を送っています。もしこの信号送信中に画面の内容を書き換えても、特に画面の上のほうだと既に信号送信済みで、次の60分の1秒が来るまでテレビ画面には表示されないかもしれませんし、その前に移動して、結局実際には表示されないかもしれません。また、ちらつきの原因にもなります。これを防ぐため、画面の書き換えはなるべく残りの約2.5ミリ秒の間に行うのが理想的です。
 これを実現するため、本システムでは符号なし16ビットのグローバール変数「drawcount」が用意されています。テレビへの映像信号出力が最下行まで終了すると、drawcountは1増加するようになっています。この変数の変化を監視することで、理想的な画面書き換えのタイミングを知ることができます。
 上記プログラムでは、まずdrawcountに0を代入し、drawcountが0の間はasm("wait")という命令を実行して、CPUをスリープモードにしています。空命令(「;」だけを書く)でもよいのですが、何もしない間はスリープモードにして、なるべく省電力化を図っています。
 スリープモードに入るとCPUは動作を停止しますが、タイマーは動作しており、ビデオ出力のタイミングが来ると、タイマー割り込みが発生し、割り込みプログラムを処理します。割り込みから戻ると、スリープモードからは抜けているので、再度drawcountをチェックし、0であれば再度スリープ、0でなければウェイトループから抜け出ることになります。
 実際のところ、ここまでの理解はしなくてもゲームプログラムは作れますので、ウェイトには決まり文句のように上記の60分の1秒ウェイトを使用すればよいでしょう。  


ボタン入力チェック

 またgame1.cを下記プログラムに書き換えてビルドし、実行させてください。今度は上下左右ボタンを押すと、その方向にキャラクターが移動します。ソースはこちらでもダウンロードできます。

					
#include <plib.h>
#include "composite32-high4.h"
#include "lib_composite32-high.h"

//外付けクリスタル with PLL (15倍)
#pragma config PMDL1WAY = OFF, IOL1WAY = OFF
#pragma config FPLLIDIV = DIV_1, FPLLMUL = MUL_15, FPLLODIV = DIV_1
#pragma config FNOSC = PRIPLL, FSOSCEN = OFF, POSCMOD = XT, OSCIOFNC = OFF
#pragma config FPBDIV = DIV_1, FWDTEN = OFF, JTAGEN = OFF, ICESEL = ICS_PGx1

const unsigned char bmp[14*13]={
	0,0,0,0,0,2,2,2,2,0,0,0,0,0,
	0,0,0,2,2,2,2,2,2,2,2,0,0,0,
	0,0,2,2,2,2,2,2,2,2,2,2,0,0,
	0,2,2,2,7,7,2,2,2,2,7,7,2,0,
	0,2,2,7,7,7,7,2,2,7,7,7,7,0,
	0,2,2,7,7,8,8,2,2,7,7,8,8,0,
	2,2,2,7,7,8,8,2,2,7,7,8,8,2,
	2,2,2,2,7,7,2,2,2,2,7,7,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,2,2,2,2,2,2,2,2,2,2,2,2,
	2,2,0,2,2,2,0,0,2,2,2,0,2,2,
	2,0,0,0,2,2,0,0,2,2,0,0,0,2
};

unsigned short keystatus=0,keystatus2,oldkey; //最新のボタン状態と前回のボタン状態

void keycheck(void){
//ボタン状態読み取り
//keystatus :現在押されているボタンに対応するビットを1にする
//keystatus2:前回押されていなくて、今回押されたボタンに対応するビットを1にする
	oldkey=keystatus;
	keystatus=~KEYPORT & (KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT | KEYSTART | KEYFIRE);
	keystatus2=keystatus & ~oldkey; //ボタンから手を離したかチェック
}

void main(void){
	int x,y;

	//ポートの初期設定
	TRISA = 0x0010; // RA4は入力
	CNPUA = 0x0010; // RA4をプルアップ
	ANSELA = 0x0000; // 全てデジタル
	TRISB = KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// ボタン接続ポート入力設定
	CNPUBSET=KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// プルアップ設定
	ANSELB = 0x0000; // 全てデジタル
	LATACLR=2;// RA1=0(ボタンモード)

	init_composite(); // ビデオ出力システムの初期化

	x=100; //横位置
	y=100; //縦位置

	while(1){
		//キャラクター表示
		putbmpmn(x,y,14,13,bmp);

		//60分の1秒ウェイト
		drawcount=0;
		while(drawcount==0) asm("wait");

		//キャラクター消去
		clrbmpmn(x,y,14,13);

		//ボタン入力により移動
		keycheck();
		if(keystatus & KEYUP   ) y--; //上ボタン押された
		if(keystatus & KEYDOWN ) y++; //下ボタン押された
		if(keystatus & KEYLEFT ) x--; //左ボタン押された
		if(keystatus & KEYRIGHT) x++; //右ボタン押された

		//端チェック
		if(x>200) x=200;
		else if(x<0) x=0;
		if(y>200) y=200;
		else if(y<0) y=0;
	}
}
					
					

 回路図にある通り、本システムにはボタンが6個搭載されていて、PORTBの各ピンで読み取るようになっています。main関数の初期設定で、CNPUBポートをセットして内部でプルアップを有効にしていますので、ボタンを離している時が1、押している時が0となります。
 KEYSTART、KEYFIRE、KEYUP、KEYDOWN、KEYLEFT、KEYRIGHTはcomposite32-high4.hで定義されている定数で、各ボタンが接続されているPORTBのそれぞれのビットが1になっています。(例えば下ボタンはRB7につながっているのでKEYDOWNは0x0080。)
 keycheck関数を呼び出すことで、グローバル変数keystatusにボタンの状態を読み込みます。考えやすいようにビット反転しているので、先ほどの各定数とandをとって0以外だとそのボタンが押されていると判断できます。
 本プログラムではループで60分の1秒ごとに1回ボタン読み込みを行い、上下左右ボタンが押されているかチェックして、押されている方向に1ドット移動させています。そのため、1秒間押し続けると60ドット分移動します。上下、左右とも200ドット分移動できるようにしたので、3秒ちょっとで端から端に移動となります。ちなみに、右ボタンと上ボタンを同時に押すと、右上に移動します。
 ところで、keycheck関数ではkeystatus2というグローバル変数も更新しています。試しにループ内の上下左右ボタンチェックの4つのif文の条件を全てkeystatusからkeystatus2に変更してみてください。ボタンを押すと1ドット動きますが、それ以上動きません。ボタンを離してもう一度押すと、また1ドットだけ動きます。ミサイル発射ボタンのようにボタンを離したことをチェックしたい場合は、こちらを使ってください。(60分の1秒以内に連打しても反応しません。でも普通無理ですよね。)


音声出力

 テレビゲームの重要な要素に、効果音とBGMがあります。本システムにはモノラルの簡易的な音声システムを搭載しました。回路図で見るとRB13端子が抵抗をはさんでテレビの音声入力に接続されます。この状態で、RB13を適当な周期でオン、オフを繰り返すと音が鳴るという仕組みです。
 いちいちプログラムでオン、オフさせるのは大変ですので、PICの出力コンペア(OC)機能を使用します。ピン割り当て機能でOC4の出力をRPB13に設定し、OC4の設定をベースタイマーTimer3、Toggleモードとします。これでTimer3とOC4を有効にすると、Timer3の周期設定値PR3にしたがってRB13端子がオン、オフを繰り返し、テレビのスピーカから音が鳴ります。PR3の設定値は以下の計算で求められます。

  PR3=Timer3周波数÷(出力したい周波数f×2)

 Timer3周波数は3579545Hz(水晶発振子)×15(PLL設定値)÷8(プリスケーラ設定値)とします。また、音階が1オクターブ(12音階)上がるごとに周波数fは2倍となるため、fは2のべき乗の指数関数で表され、一般的に以下の計算式が使われています。

  f [Hz]=440×2(N/12)

 Nは基準のAの音(ラ)との差分値で、黒鍵を含め鍵盤の1つ右にいくと+1、左にいくと-1とするものです。これらを計算したものが右の表になります。これをあらかじめ定数配列で保持しておくと、簡単に出したい音程を出力することができます。なお、出力停止する場合はPR3=0とします。

音階データ

 以下のプログラムは、低いC(ド)から3オクターブ分、1秒ずつ半音単位で順に鳴らすものです。ソースはこちらでもダウンロードできます。

					
#include <plib.h>
#include "composite32-high4.h"
#include "lib_composite32-high.h"

//外付けクリスタル with PLL (15倍)
#pragma config PMDL1WAY = OFF, IOL1WAY = OFF
#pragma config FPLLIDIV = DIV_1, FPLLMUL = MUL_15, FPLLODIV = DIV_1
#pragma config FNOSC = PRIPLL, FSOSCEN = OFF, POSCMOD = XT, OSCIOFNC = OFF
#pragma config FPBDIV = DIV_1, FWDTEN = OFF, JTAGEN = OFF, ICESEL = ICS_PGx1

//sounddata配列 低いド〜3オクターブ分の周期カウンタ値、PR3に書き込むと音程設定される
const unsigned short sounddata[]={
	25653,24213,22854,21572,20361,19218,18139,17121,16160,15253,14397,13589,
	12826,12106,11427,10786,10180, 9609, 9069, 8560, 8080, 7626, 7198, 6794,
	 6413, 6053, 5713, 5393, 5090, 4804, 4534, 4280, 4040, 3813, 3599, 3397,
	 3206
};

void main(void){
	int i;

	//ポートの初期設定
	TRISA = 0x0010; // RA4は入力
	CNPUA = 0x0010; // RA4をプルアップ
	ANSELA = 0x0000; // 全てデジタル
	TRISB = KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// ボタン接続ポート入力設定
	CNPUBSET=KEYSTART | KEYFIRE | KEYUP | KEYDOWN | KEYLEFT | KEYRIGHT;// プルアップ設定
	ANSELB = 0x0000; // 全てデジタル
	LATACLR=2;// RA1=0(ボタンモード)

	RPB13R=5;//RPB13ピンにOC4を割り当て
	OC4R=0;
	OC4CON=0x000b;// Timer3ベース、Toggleモード
	OC4CONSET=0x8000;//OC4スタート
	T3CON=0x0030;// プリスケーラ1:8
	PR3=0;
	T3CONSET=0x8000;// タイマ3スタート

	init_composite(); // ビデオ出力システムの初期化

	while(1){
		for(i=0;i<=36;i++){
			//60分のn秒ウェイト
			drawcount=0;
			while(drawcount<60) asm("wait");

			PR3=sounddata[i];//音程セット
		}
	}
}

 ここでは単純にするため、音階値を0から36まで順に出力しましたが、楽譜データとして音階値と音の長さを配列に保持しておき、音階値から周期設定値に変換して順に出力すれば、曲を演奏することができます。実際のゲームのBGMとなると、ゲームを進行させながら曲演奏を行う必要があります。私の場合、60分の1秒ごとに呼び出す音楽演奏関数を用意し、その関数内ではカウンタをチェックして、必要であれば次の音に更新するようにしています。
 また、BGMとは別にミサイル発射音や爆発音といった効果音については、本システムではあまりリアルなものは再現できませんが、音程を短時間ごとに変更するなど、工夫次第でそれなりのものを作ることができます。


実際のゲームサンプルプログラム

 ここまでの解説で、ワンチップテレビゲームに必要な機能はだいたい揃いました。あとは、実際にゲームを設計し、プログラムを製作するだけです。とはいえ、いきなり作るのは大変なので、シューティングゲームのひな形となるサンプルプログラムを用意しました。メインプログラムはこちらとなります。ライブラリやヘッダーファイルはこれまでと同じものをプロジェクトに追加して使います。プロジェクトのファイルをまとめてダウンロードする場合は、下記のダウンロードファイルをご使用ください。念のためコンパイル済みのHEXファイルも入れておきます。

サンプルゲーム画面

 このゲームは自機を上下左右のボタンで移動して、FIREボタンでミサイルを発射し、単純に動き回る敵を撃つだけのおもしろくも何ともないものですが、敵キャラやミサイルの動作、BGMや効果音の鳴らし方、ステージクリアや自機の死亡、ゲームオーバーなどの処理方法といった、シューティングゲームに必要な基本機能と構造が、短いプログラムの中に網羅されているので、これに肉付けを行っていくことで、より複雑なゲームに仕上げることができると思います。インベーダーゲームのような少し複雑なゲームへの応用も簡単にできるでしょう。実際、私がこれまでに製作したパックマンVELUDDAといったゲームも、このサンプルプログラムとほぼ同じ骨組みで作られています。
 ソースファイルにはコメントをたくさん書きましたので、シューティングゲームプログラムの作り方の参考にもなると思います。ぜひハードの製作と合わせて、自作のゲームを製作して遊んでみてください。いいものができたら、公開してみられてはいかがでしょうか。

関連ファイル一式のダウンロード

(2015.11.21) 今後のシステム対応のため、ポートの設定値を変更

(2015.1.12)




 ご質問等ありましたら、こちらの掲示板をご利用ください。

Copyright (C) KenKen All Rights Reserved.