iPhone/iPad用ドット絵エディタ「EDGE touch」

最近のエントリー

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_03.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_03.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、スライムが2個表示されると思います。
というわけで今回は、キー入力により複数のキャラを動かす処理ついて解説したいと思います。

複数キャラの扱い

今回は、こいつら(slime.bmp 64×32)を使用して複数のキャラクタを表示、移動させてみたいと思います。

というわけでサンプルコードを見てみます。

//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){

	<略>

	//座標
	int cx[2]={160-16,480-16},cy[2]={240-16,240-16};
	
	//キーテーブル
	unsigned char key_up[2]={38,104},key_down[2]={40,98},
		key_left[2]={37,100},key_right[2]={39,102};
	
	//その他の変数
	int i,mv=3;

	//メインループ
	while(1){
		WaitSet();			//現在の時間(単位:ミリ秒)取得
		ClearScreen(lpBack);		//バックバッファ初期化
		DdTextOut(lpBack,0,0,"カーソルキー、テンキー(4,6,8,2)で移動 pauseキーで終了",255);

		for(i=0;i<2;i++){
			//キャラクター移動
			if(keyg[key_up[i]])	//↑
				cy[i]-=mv;
			else
			if(keyg[key_down[i]])	//↓
				cy[i]+=mv;
			else
			if(keyg[key_left[i]])	//←
				cx[i]-=mv;
			else
			if(keyg[key_right[i]])	//→
				cx[i]+=mv;
			
			//キャラクタ表示
			BltClip(lpBack,cx[i],cy[i],32,32,lpWork,32*i,0,1);
		}
		
		Flip();				//フィリップ
		Wait(1000/60);			//メッセージループへ
	}

初心者だと、つい1キャラづつ変数用意(例えば int,cx1,cx2,cx3・・・)し、処理も1キャラづつ用意しそうですが、そんな必要はありません。

座標等の変数を配列(もしくはクラス・構造体の配列)で用意する事により、ループ処理で複数のキャラを処理できるというわけです。

キーボードのお話

今回は2つのキャラクターを同時に表示し、キー入力も4方向×2キャラという事で8つのキーを扱っているのですが、AT互換機(DOS/V)でキーボードを一気にたくさん押すと、一部のキーの反応が無くなる場合があります(PC-9821シリーズでは大丈夫なようですが)。
スペースソルジャーでも、2人で同時に遊んでいると、どちらかの機体が左上に行けなくなるという現象が起きました。
これはハードウェアの仕様という事でプログラマーにはどうする事も出来ないようです。
そういった場合にはジョイパッドに逃げるしかありませんね(^^;

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_02.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_02.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、カーソルキーを押すと、スライム(?)が移動すると思います。
というわけで今回は、キー入力によるキャラクタ移動について解説したいと思います。

キャラクタの表示

では、まずキー入力の前に、キャラクターの表示について簡単に説明します。
まず、キャラクター(slime.bmp 32×32)を作業用バッファ(lpWork)に読み込んでおきます。
画面に表示するだけなら、作業用バッファからプライマリサーフェイスへBltFastとかで転送すれば、それで終わりですが、今回は、キャラクタの座標が変わったら、新しい座標へ移動するようなプログラムを組まなければなりません。
単に毎回キャラクタの座標へキャラクタを表示し続けるだけでは、元の座標に描かれていた絵がそのまま残ってしまいますよね?
そこで、多少工夫(?)し、残骸が残らないようにしてやる必要があります。

今回のサンプルでは、もっとも簡単な方法でそれらを実現しています。
ただ、バックバッファを全部消し、キャラを表示、フリップするだけ。
これなら、毎回バックバッファを全消去しているため、キャラの残骸が残らないと言うわけです。

見れば解りますね(^^;説明するまでもないと思いましたが・・・。

int cx,cy;//キャラクタのx,y座標

略
	ClearScreen(lpBack);
	BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1);
	Flip();

やってはいけない?

さて、キー入力があった場合にウィンドウプロシージャが呼ばれることは前回解ったと思います。
で、実際にキー入力があった場合にキャラクタを移動させるようなプログラムを組むにはどうすればいいでしょう?
まず、初心者がよく考えそうな事は、キャラクタの座標をグローバル変数で用意し、ウィンドウプロシージャが呼ばれて、特定のキーが押されたら、そこで座標を変更するというもの。

実際に組むと、たぶんこんな感じ。
(キーコードについては前回のプログラムを実行して調べてください(爆))

int cx,cy;//キャラクタのx,y座標

略

//----------[ ウィンドウプロシージャ ]----------------------------------------------------------
HRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam){
	
	switch(iMessage){

	略

	//キー押された
	case WM_KEYDOWN:
		if(wParam==37)	//「←」キー
			cx-=5;
		else
		if(wParam==39)	//「→」キー
			cx+=5;
		・
		・
		・
		ClearScreen(lpBack);
		BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1);
		Flip();

	return(TRUE);

この方法でも、キャラクタは動くには動きます。
さて、テキストで文字を打つときの事を思い浮かべてください。
‘A’キーをずっと押しっぱなしにしていると、’A’と一度表示され、しばらくしてから連続的に’A’が表示されると思います。
ウィンドウプロシージャもこのタイミングで呼ばれるため、キャラクタはカクカク動作する事になり、非常にカッコ悪いし、ゲームにも使えません。

仮想キーマップのお話

というわけで、↑の問題を解決すべく、仮想キーマップなるものを作成します。
まぁ、これは私が勝手に考えたものなので正しい方法とは言い切れません(爆。

では、プログラムを見てみましょう。


/*  グローバル変数群   */
char keyg[256];	//仮想キーマップ

略

//----------[ ウィンドウプロシージャ ]----------------------------------------------------------
HRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam){

	略

	//キー押された
    	case WM_KEYDOWN:
		//pauseキー(キーコード19)が押されたら終了
		if(wParam==19)
			Quit();
		keyg[wParam]=1;
		return(TRUE);

	//キー離された
	case WM_KEYUP:
		keyg[wParam]=0;
		return(TRUE);

では、まず256個の配列を確保します(keyg)。
キーが押され「WM_KEYDOWN」が送られてきたらkeyg[wParam]に1を代入します(wParamは、キーコードを表します)。
で、キーが離され「WM_KEYUP」が送られてきたらkeyg[wParam]に0を代入します。
これだけです(^^;これで、ウィンドウプロシージャ以外の関数内でもkeyg[キーコード]を参照する事によって
そのキーが押されている(値が1)のか、押されていないか(値が0)を判断出来ると言うわけです。

同期のお話

さて、いつでもキーの状態を取得出来るようになったので、実際にちょっと組んでみましょうか。
こんな感じですかねぇ。


//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){

	MSG msg;
	
	<略>

	int cx=320-16,cy=240-16,mv=3;
	//メインループ
	while(1){
		ClearScreen(lpBack);		//バックバッファ初期化
		DdTextOut(lpBack,0,0,"カーソルキーで移動 pauseキーで終了",255);

		//キャラクター移動
		if(keyg[38])	//↑
			cy-=mv;
		else
		if(keyg[40])	//↓
			cy+=mv;
		else
		if(keyg[37])	//←
			cx-=mv;
		else
		if(keyg[39])	//→
			cx+=mv;
		
		//キャラクタ表示
		BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1);
		Flip();						//フィリップ
		
		//メッセージループ
		while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
			if(!GetMessage(&msg,NULL,0,0))
				Quit();
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

このプログラムには欠点があります。
それは、マシンの処理速度(描画速度)が速ければ速い程、キャラクタの移動速度も
上がってしまうという事です。つまり、処理速度の違うパソコンによってゲーム速度が変わって
しまうというのです(まぁフリップである程度抑えられますが・・・)。

というわけで、疑似タイマー処理(?)を紹介します。以前「DirectDraw基礎 第5回」でも書いたのですが、もうちょっと変更してみます(^^;

プログラムはこんな感じです(今回のサンプルコードと同じ)

/*  グローバル変数群 */
DWORD wait_time;	//ウェイト用

略

//----------[ ウェイト開始時間設定(謎 ]--------------------------------------------------------
void WaitSet(void){
	wait_time=timeGetTime();
}
//----------[ ウェイト ]------------------------------------------------------------------------
void Wait(DWORD msec){
	MSG msg;
	do{
		while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
			if(!GetMessage(&msg,NULL,0,0))
				Quit();
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}while(timeGetTime()<wait_time+msec);
}
//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){

	
	<略>

	int cx=320-16,cy=240-16,mv=3;
	//メインループ
	while(1){
		WaitSet();			//現在の時間(単位:ミリ秒)取得
		ClearScreen(lpBack);		//バックバッファ初期化
		DdTextOut(lpBack,0,0,"カーソルキーで移動 pauseキーで終了",255);

		//キャラクター移動
		if(keyg[38])	//↑
			cy-=mv;
		else
		if(keyg[40])	//↓
			cy+=mv;
		else
		if(keyg[37])	//←
			cx-=mv;
		else
		if(keyg[39])	//→
			cx+=mv;
		
		//キャラクタ表示
		BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1);
		Flip();					//フィリップ
		Wait(1000/60);				//メッセージループへ
	}

さて、まずメインループの最初でWaitSet()関数を呼び出しています。
これは自作関数なのでコードを見てもらえば解ると思いますが、グローバル変数wait_timeに、timeGetTime()関数を呼び出し、現在の時間をミリ秒(1秒=1000ミリ秒)で保存しておきます。
その後、メインループでいろいろな処理をし、最後にWait()関数が呼ばれています。
これも自作関数です。
このWait()関数の内容は主にメッセージループなのですが、現在の時間(timeGetTime())が、最初に記憶しておいたwait_time+Wait()関数の引数msecよりも小さい間はひたすたメッセージループを回しています。
つまり、WaitSet()を呼び出した時間から、Wait()の引数msecミリ秒時間が経つまでWait()関数内で止まっている事になります。

Wait()の引数を1000にすれば、秒間1回更新、500で2回更新・・・。
1000/nでn回更新となります。
ただ、この方法にも欠点があるのですが、それはWaitSet()を呼び出してから、いろいろな処理をし、Wait(DWORD msec);を呼ぶ場合、「いろいろな処理」の時間がWaitの引数「msec」より大きくなってしまうと、
Wait()関数内は、ほぼ素通り(1回はメッセージループの処理をやります)になり、いわゆる「もたつき」だとか「処理落ち」と言った現象になります。
普通、ゲームでは秒間30回更新(1ループ33msec)を目安にしたほうがいいですね。
これより小さいと、動作がカクカクに見えてしまうし、大きいと、処理落ちする可能性が大きくなります。
まぁ場合によりますので、いろいろ試してみてください(^^;

同期のお話 オマケ

さて、上では疑似タイマー処理なるものを紹介しましたが、
タイマー系の処理の種類はいろいろあります。
今回紹介したものは、毎回処理される時間を一定時間に合わせていますね?
この考え方は、よく固定フレームとか言います。
固定フレームでは、プログラムが簡単になる分、「もたつき」が起こりうる可能性が出てしまいます。
また、例えば秒間30回更新(30fps)に設定した場合、とても高性能なマシンで動かしても30FPSです。
ちょっとCPUがもったいない気もしますよね?
まぁ、60FPS設定にしておいて、処理がおいつかない場合は、描画をスキップするなんて事もよくやるようですが・・・。

ちなみに固定フレームに対するものは、変動フレームです。
これは、前回から今回のループに掛かった時間を計測しておき、
「距離=時間×速度」と言ったような式を用いてゲームを進行させる方法です。
この方法では、プログラムが複雑になったり、浮動小数点を使用する必要があったりしますが、マシンが高性能であればある程、ゲームが滑らかになります。

固定か変動かはケースバイケースなので、ゲームに合った方を使用してください。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_01.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_01.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、何かキーを押すと、そのキーコードが画面に表示されると思います。
というわけで今回は、キー入力について解説したいと思います。

ウィンドウメッセージ

前にDirectDraw講座でも触れましたが、Windows上ではアプリケーションとWindows(OS)がコミュニケーションをとるために、ウィンドウメッセージというのをやりくりします。

例えばキーが押されたらWindowsがアプリケーションに対して「キーが押されたぞ」というメッセージを送信します。
そしたらアプリ側はメッセージループ中、そのメッセージを受信し、ウィンドウプロシージャと呼ばれる関数を呼び出します(ウィンドウプロシージャは自分で制作する事になります)。

ではサンプルコードを見ていきます。

	
//----------[ ウィンドウプロシージャ ]----------------------------------------------------------
HRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam){
	
	switch(iMessage){

	略

	//キー押された
	case WM_KEYDOWN:
		//pauseキー(キーコード19)が押されたら終了
		if(wParam==19)
			Quit();

		ClearScreen(lpBack);
		DdTextOut(lpBack,0,0,"Pause(STOP)キーで終了",1);
		sprintf(tmp,"key=%d",wParam);
		DdTextOut(lpBack,0,16,tmp,1);
		Flip();

	return(TRUE);

メッセージの種類は「UINT iMessage」に格納されています。
ですから、switch文で分岐してやります。
キーが押された時の値は「WM_KEYDOWN」(マクロ)です。
そして、wParamに、押されたキーのキーコードが格納されています。
このキーコードについては、資料不足で(爆)どの値がどのキーかは判りません。
ですが、キーが押されたら、そのキーコードを画面に表示する事で、キーのキーコードが判るというわけです。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_13.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_13.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、1ドットの荒いタイル(謎)がスクロールしていると思います。

というわけで、今回は低解像度(MODE X)モードを使ってみたいと思います。

ModeX

DirectDrawでは320×240、320×200と言った低解像度のディスプレイモードをサポートしています。
こういった特殊(?)なディスプレイモードを使用する場合には、ModeXとよばれるものを使用しなければなりません。
「ModeX」が一体何を意味するかは謎として、とにかく320×240モードを使用したい場合にはModeXの設定をすればいいのです(ォ。

使い道として、秒間60枚くらいは画面更新をしたいゲーム(ハデなアクションや格闘)、3D等で描画時間を少しでも減らせるように低解像度にするといった事が考えられます。
ただ、私が試したところ、ModeXを使用すると、一部の環境で不都合が生じる事があります。
そんな時はコントロールパネルのDirectXのアイコンをダブルクリックし、DirectDrawの項目の中の「Use ModeX for 320 for X(?)」みたいな感じのチェックを外してみてください。
たぶん正常に動作すると思います。

では実際にプログラムするにはどうしたら良いでしょう。コードを見てみましょう。

BOOL StartDirectDraw(HWND hw){

	<略>

	//協調レベル設定
	if(lpDD->SetCooperativeLevel(hw,DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE | DDSCL_ALLOWMODEX)!=DD_OK)
		return FALSE;

	//解像度設定
	if(lpDD->SetDisplayMode(320,240,8)!=DD_OK)
		return FALSE;	

自作関数「StartDirectDraw」内の協調レベル設定部分の引数に、「DDSCL_ALLOWMODEX」を追加してやります。
これで、DirectDrawがModeXを使用するように設定出来ました。
次に、解像度設定で、320×240×8(256)を指定し、この瞬間に画面が切り替わります。
とりあえず、DirectDrawの設定はこれで終わりです(^^;

ただ、ModeXにはいくつか制約があります。
まず、プライマリサーフェイスへデータを転送出来ないという点、次にプライマリサーフェイスのロック、最後にプライマリサーフェイスに対するGetDCの使用です。
要するにプライマリサーフェイスにアクセス出来ないと言う事です。
ですから、普段は複合プライマリサーフェイスを作成してバックバッファからフリップしてフロントバッファに表示させるしか方法がありません。
これらを除けばバックバッファやオフスクリーンサーフェイスの扱いは今まで通り出来ます。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、をお持ちの方はVisual C++でプロジェクトファイルを開いてください(「ddraw_12.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「ddraw_12.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化し、流れ星のようなものが表示されると思います。

というわけで、今回はサーフェイスへ直接アクセスする方法を書いていきたいと思います。

ダイレクトアクセス

まずサーフェイスへ直接アクセスするとはどういう事なのかを簡単に説明します。
サーフェイスは、ビデオメモリもしくはシステムメモリ内に作成されます。
メモリは1次元配列で管理されており、DirectDrawでは、サーフェイスが存在しているメモリの先頭アドレスを取得する事が出来ます。
ですから、このアドレスを使って、1次元配列と同じようにサーフェイス扱う事ができると言うわけです。
と言っても、使用するのは「点を描く」と言った処理程度でしょう。

では、実際にサーフェイスへの先頭アドレスを取得する方法を記述します。
サンプルコードを見てみましょう。

	unsigned char *p;
	DDSURFACEDESC desc;

	<略>
		
		//サーフェイスをロック
		ZeroMemory(&desc,sizeof(DDSURFACEDESC));
		desc.dwSize=sizeof(DDSURFACEDESC);
		lpBack->Lock(NULL,&desc,DDLOCK_WAIT | DDLOCK_SURFACEMEMORYPTR,NULL);
		p=(unsigned char *)desc.lpSurface;

		//データを書き込む
		for(i=0;i<STAR;i++){
			p[x[i]+(int)y[i]*desc.lPitch]=sp[i];
			if((y[i]+=sp[i]/4.0f)>=480)
				y[i]-=480;	
		}

		//ロックを解除
		lpBack->Unlock(desc.lpSurface);

今回はサーフェイス作成時にも使用した構造体「DDSURFACEDESC」型を使用するので、最初にこれを初期化しておきます(ZeroMemory等)。
そうしたら、サーフェイスのメンバである「Lock」という関数を呼び出します。
サーフェイスをロックする事により、サーフェイスへの先頭アドレスが取得出来ます。

では、Lockの構文を書いておきます。

書式 HRESULT Lock( LPRECT lpDestRect, LPDDSURFACEDESC lpDDSurfaceDesc, DWORD dwFlags, HANDLE hEvent );
lpDestRect ロックしたい領域を識別するRECT構造体のアドレス。NULLであれば、全サーフェスがロックされる
lpDDSurfaceDesc サーフェスについての情報を格納する DDSURFACEDESC 構造体へのポインタ。
dwFlags
DDLOCK_SURFACEMEMORYPTR 指定した矩形の先頭への有効なメモリ ポインタを返さなければならないことを表すフラグ。矩形が指定されない場合、一番上のサーフェスへのポインタが返される。
DDLOCK_WAIT 何らかの原因によってロック(アドレス取得)が出来ない場合に、出来るまで待つという指定
hEvent 現在は使用していないのでNULLを指定しなければならないらしい
戻り値 成功した場合はDD_OKが返ってくるらしい。

こうすると、DDSURFACEDESC構造体のメンバであるlpSurfaceに、サーフェイスの先頭アドレスが格納されるので、そのアドレスをあらかじめ用意しておいたunsigned char型(256色なので)のポインタ変数pへ代入してやります。

では、このポインタの扱い方(サーフェイスへのアクセス)を説明します。
前にも記述しましたが、メモリは1次元配列で構成されているので、「座標X、Yへ5を代入」という場合にp[y][x]=5;とは書けません。
メモリは座標(0,0)から右へ順に行き、突き当たったらY座標が1ドット下へいき、X座標が0になる・・・を繰り返す形で存在しています(謎。
ですから、p[0]が(0,0)になります。
それで、本来ならばp[640]が座標(0,1)つまりp[x+y*640]というように考えるはずなのですがビデオカードによっては、自動的にサーフェイスの横幅を少し増やして、そこをキャッシュとして使用するものがあります。
そして、DDSURFACEDESC構造体のメンバであるlPitchには、このキャッシュの部分を含めた正確な横幅が格納されています。
ですから、サーフェイスメモリへアクセスするにはp[x+y*desc.lPitch]と表すのが正確となります。

一通り、アクセスし終えたら、ロックを解除してやる必要があります。

書式 HRESULT Unlock( LPVOID lpSurfaceData );
lpSurfaceData Lockによって取得され、アンロックすべきサーフェスのアドレス。このパラメータは、対応する Lock 呼び出しで lpDestRect パラメータに NULL を渡して全サーフェスをロックした場合に限り、NULL とする。

ちなみに、全体をロックしなかった場合は、desc.lpSurfaceを引き渡す事になります。

戻り値 成功した場合はDD_OKが返ってくるらしい。

これで、一通り終わりです。
実際にサーフェイスへアクセスするのは、ロックからアンロックする間だけにしてください。
ロックしてサーフェイスポインタを取得し、直ぐにアンロックしてしまった後に、サーフェイスへアクセスする事は一応出来ますが、
ビデオカードによっては上手くいかない場合があるので、止めた方がいいです(確認済み)。
それと、ロックという処理は、結構遅いので1ループに1度というのが理想です。
1ループに何度もロック、アンロックを繰り返すのは速度低下の原因となります。
それから、ロックしたあと、アンロックしないと、そのサーフェイスへの書き込み(GDIやBltFast等による転送)が不自由になるので注意してください。

ちなみに、ビデオメモリ内のサーフェイスへのアクセス(読み込み&書き込み)は非常に遅いので、一度システムメモリ内へコピーし、システムメモリ内をいじってから、ビデオメモリ内のサーフェイスへ転送する方がいいかもしれません。

記事検索

コミュニティ

Banner designed by でゅぽる様
ドット絵掲示板
ドット絵掲示板
自作の「ドット絵」を投稿することができます。ドッターさん同士の交流の場としてご利用ください。
TakaboSoft Wiki
TakaboSoft Wiki
ソフトウェアに関する質問・不具合報告・要望などを書き込むことができます。