1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コンソールアプリでの無理やりゲーム→WinformsのTextBox

Last updated at Posted at 2025-10-29

概要

以前,C# コンソールアプリ 初歩の初歩とかいう話で,無理やりコンソールアプリでSTGとかやろうとして「操作性がクソすぎて無理ゲー」という結論に達したことがあったのだが,文字を表示できればよいだけならば TextBox 使うのでよくね? とか思った.なんか急に.

操作性がどうのいう話のほかにも,必要ならば複数個の表示域を設けたりするとかも楽だろうし,ちょっとしたメニューとかもあるだけで断然便利だ.

……というわけで,件のSTGをものすごく雑に Winforms に移植してみた.

方針

  • 表示域 → TextBox を使う:
    • MultilineReadOnlytrue にしたやつを用意して文字列表示先として使う.
    • Font には等幅なやつを設定しておく.
    • ForeColor , BackColor はお好みで.
    • 何なら適当に Dock.Fill とか設定しておく.
  • ゲームループ(処理をループさせる手段) → Timer で済ませれば良いよね
  • キー入力 → System.Windows.Input.Keyboard.IsKeyDown() とかで.
    • イベントでやるのは面倒なので.
    • これ使うのに PresentationCore.dllWindowsBase.dll への参照を追加する必要があった.

コード

移植部

実装はほぼ以前のコンソールアプリの STG のコードそのまま.

  • コピペして→必要に応じてメソッドに分けたりしただけなレベル.
    • while でループしていたのをやめ,1回分の更新処理を Update() とする
    • キー入力のところを上記の System.Windows.Input.Keyboard.IsKeyDown() に変更
    • TextBox に表示するための文字列を返すメソッドを追加とか
移植コード(280行くらい)
using System;
using System.Linq;
using System.Windows.Input;

namespace CharSTG
{
	//1行分のバッファ
	class LineBuffer
	{
		public LineBuffer(int W) { m_Buff = new char[W + 1]; m_Buff[W] = '|'; }
		public void Clear() { for( int i = 0; i < m_Buff.Length - 1; ++i ) { m_Buff[i] = ' '; } }
		public void Write(int left, string s) { for( int i = 0; i < s.Length; ++i ) { m_Buff[left + i] = s[i]; } }
		public void Write(int left, char c) { m_Buff[left] = c; }
		public override string ToString(){	return new string(m_Buff);	}

		private char[] m_Buff;
	}

	//座標
	class Pos
	{
		public Pos(int X = 0, int Y = 0) { x = X; y = Y; }
		public int x { get; set; }
		public int y { get; set; }
	}

	class STG
	{
		//自機
		static string[] MyFig = {
			@"  |",
			@"!=H=!"
		};
		const int MyFigCX = 2;  //MyFig[]内での機体中心位置座標
		const int MyFigCY = 1;

		//敵の弾と自機との当たり判定
		static bool MyFig_CollisionCheck(Pos Bullet, Pos FigC)
		{ return (Math.Abs( Bullet.x - FigC.x ) <= 1 && (Bullet.y == FigC.y || Bullet.y == FigC.y - 1)); }

		//敵
		static string[] EnemyShip = {
			@"  Z   _ _   Z",
			@" E]._[:=:]_.E]",
			@"<  }^=MOM=^{  >",
			@" [|'  Y^Y  `|]",
			@" {|         |}",
			@"  !         !"
		};
		const int EnemyShipCX = 7;  //EnemyShip[]内での機体中心位置座標
		const int EnemyShipCY = 2;

		//こっちの弾と敵との当たり判定
		static bool EnemySip_CollisionCheck(Pos Bullet, Pos EnemyC)
		{ return (Math.Abs( Bullet.x - EnemyC.x ) <= 2 && Math.Abs( Bullet.y - EnemyC.y ) <= 1); }

		//敵の弾
		class EnemyBullet
		{
			//発射時のデータ初期化
			public void Setup(int x, int y, Random rnd)
			{
				m_Count = 0;
				m_x = x;
				m_y = y;
				//Update() が m_MoveXFreq 回実施される毎に,X座標が1だけ動く,という感じ
				m_MoveToLeft = ((rnd.Next() & 0x01) == 0);    //左右どっちに動くか
				m_MoveXFreq = 1 + rnd.Next( 8 );    //動く頻度
													//表示用の文字をランダムに決める
				img = Chars[rnd.Next( Chars.Length )];
			}
			//位置更新
			public void Update()
			{
				++m_y;
				++m_Count;
				if( m_Count >= m_MoveXFreq )
				{
					m_Count = 0;
					m_x += (m_MoveToLeft ? -1 : 1);
				}
			}
			//位置取得
			public Pos pos { get { return new Pos( m_x, m_y ); } }
			//この弾の表示用の文字を取得
			public char img { get; private set; }

			private int m_x;
			private int m_y;
			private int m_Count;
			private bool m_MoveToLeft;
			private int m_MoveXFreq;
			private static char[] Chars = new char[3] { '*', '%', '+' };
		}

		//自機,敵 の絵をバッファの指定位置にコピーする処理
		static void DrawChar(LineBuffer[] ImgBuff, int Left, int Top, string[] Lines)
		{
			foreach( string s in Lines )
			{
				ImgBuff[Top].Write( Left, s );
				++Top;
			}
		}

		//ゲーム世界の広さ
		const int W = 60;
		const int H = 24;

		//ゲーム状態データ
		LineBuffer[] m_ImgBuff;
		Pos m_MyPos;
		int m_MyHP;
		Pos[] m_MyBullets;
		int m_nMyBullets;

		Pos m_EnemyPos;
		int m_EnemyHP;
		EnemyBullet[] m_EnemyBullets;
		int m_nEnemyBullets;

		Random m_Rnd = new Random();

		/// <summary>ctor. ゲーム開始状態にデータを初期化する</summary>
		public STG()
		{
			//自機側のデータ
			m_MyPos = new Pos( W / 2, H - 2 );  //位置
			m_MyHP = 6;   //HP
			m_MyBullets = new Pos[3];
			for( int i = 0; i < m_MyBullets.Length; ++i ){ m_MyBullets[i] = new Pos(); }
			m_nMyBullets = 0;

			//敵側データ
			m_EnemyPos = new Pos( W / 2, EnemyShipCY + 1 );   //位置
			m_EnemyHP = 16;   //HP
			m_EnemyBullets = new EnemyBullet[10];
			for( int i = 0; i < m_EnemyBullets.Length; ++i ) { m_EnemyBullets[i] = new EnemyBullet(); }
			m_nEnemyBullets = 0;

			//表示用のバッファ
			m_ImgBuff = new LineBuffer[H + 2];    //※ +2 は,こっちと敵の HP 表示行の分
			for( int y = 0; y < m_ImgBuff.Length; ++y ) { m_ImgBuff[y] = new LineBuffer( W ); }
			UpdateImgBuffer();
		}

		//表示内容更新
		private void UpdateImgBuffer()
		{
			//バッファクリア
			foreach( var LB in m_ImgBuff ) { LB.Clear(); }

			if( m_EnemyHP > 0 )   //バッファに敵の描画
			{ DrawChar( m_ImgBuff, m_EnemyPos.x - EnemyShipCX, m_EnemyPos.y - EnemyShipCY, EnemyShip ); }

			if( m_MyHP > 0 )  //バッファに自機の描画
			{ DrawChar( m_ImgBuff, m_MyPos.x - MyFigCX, m_MyPos.y - MyFigCY, MyFig ); }

			//バッファにこっちの弾を描画
			for( int i = 0; i < m_nMyBullets; ++i )
			{ m_ImgBuff[m_MyBullets[i].y].Write( m_MyBullets[i].x, ':' ); }

			//バッファに敵の弾を描画
			for( int i = 0; i < m_nEnemyBullets; ++i )
			{
				var P = m_EnemyBullets[i].pos;
				m_ImgBuff[P.y].Write( P.x, m_EnemyBullets[i].img );
			}

			//バッファにHPを描画
			m_ImgBuff[H].Write( 0, "M:" + new string( '@', m_MyHP ) );
			m_ImgBuff[H + 1].Write( 0, "E:" + new string( '#', m_EnemyHP ) );
		}

		/// <summary>
		/// 表示用文字列を返す.
		/// 返される内容は <see cref="Update"/> により更新される.
		/// </summary>
		/// <returns>複数行から成る(=改行を含む)文字列</returns>
		public string ViewContent()
		{	return string.Join( Environment.NewLine, m_ImgBuff.Select( LineBuff => LineBuff.ToString() ) );	}

		/// <summary>ゲームクリア状態か否か</summary>
		public bool GameClear => ( m_EnemyHP<=0 );

		/// <summary>ゲームオーバー状態か否か</summary>
		public bool GameOver => ( m_MyHP<=0 );

		/// <summary>ゲーム更新処理</summary>
		/// <returns>
		/// 更新したかどうか.
		/// 既にゲーム終了状態(クリアorゲームオーバー)である場合には何もせずにfalseを返す.
		/// </returns>
		public bool Update()
		{
			if( m_EnemyHP <= 0 || m_MyHP <= 0 )return false;

			//操作
			if( m_MyPos.x - MyFigCX > 0  &&  Keyboard.IsKeyDown( Key.Z ) ){ --m_MyPos.x; }
			if( m_MyPos.x + MyFigCX < W - 1  &&  Keyboard.IsKeyDown( Key.X ) ){ ++m_MyPos.x; }

			if( m_nMyBullets < m_MyBullets.Length  &&  Keyboard.IsKeyDown( Key.Space ) )
			{	//弾の発射
				m_MyBullets[m_nMyBullets].x = m_MyPos.x;
				m_MyBullets[m_nMyBullets].y = m_MyPos.y - MyFigCY;
				++m_nMyBullets;
			}

			{//こっちの弾の移動,敵との当たり関係の処理
				bool Hit = false;   //いずれかの弾が敵に当たったか?

				int i = 0;
				while( i < m_nMyBullets )
				{
					--m_MyBullets[i].y;

					bool ShouldRemove = false;
					if( m_MyBullets[i].y < 0 )    //場外判定
					{ ShouldRemove = true; }
					else if( !Hit && EnemySip_CollisionCheck( m_MyBullets[i], m_EnemyPos ) )
					{//敵に当たったとき
						--m_EnemyHP;
						Hit = true;
						ShouldRemove = true;
					}

					if( ShouldRemove )
					{
						m_MyBullets[i].x = m_MyBullets[m_nMyBullets - 1].x;
						m_MyBullets[i].y = m_MyBullets[m_nMyBullets - 1].y;
						--m_nMyBullets;
					}
					else
					{ ++i; }
				}

				//弾が当たった場合,敵がどこかにワープする
				if( Hit )
				{ m_EnemyPos.x = EnemyShipCX + m_Rnd.Next( W - EnemyShipCX * 2 ); }
			}

			//敵の弾発射処理
			if( (m_EnemyHP > 0) && (m_nEnemyBullets <= m_Rnd.Next( m_EnemyBullets.Length )) )
			{
				m_EnemyBullets[m_nEnemyBullets].Setup( m_EnemyPos.x, m_EnemyPos.y, m_Rnd );
				++m_nEnemyBullets;
			}
			{//敵の弾の移動,自機との当たり関係の処理
				int i = 0;
				while( i < m_nEnemyBullets )
				{
					m_EnemyBullets[i].Update();

					bool ShouldRemove = false;
					var P = m_EnemyBullets[i].pos;
					if( P.x < 0 || P.x >= W || P.y >= H ) //場外判定
					{ ShouldRemove = true; }
					else if( MyFig_CollisionCheck( P, m_MyPos ) )
					{//当たったとき
						--m_MyHP;
						ShouldRemove = true;
					}

					if( ShouldRemove )
					{
						var Tmp = m_EnemyBullets[i];
						m_EnemyBullets[i] = m_EnemyBullets[m_nEnemyBullets - 1];
						m_EnemyBullets[m_nEnemyBullets - 1] = Tmp;
						--m_nEnemyBullets;
					}
					else
					{ ++i; }
				}
			}
			//表示内容更新
			UpdateImgBuffer();
			return true;
		}
	}
}

Form 側

Fig1.png

何も説明する必要もないくらいに簡素.
ど真ん中に TextBox を配置し,あとは単にタイマイベントで前記移植コードを使うだけの実装をちょろっと書けば終了だ.
左上にキャレットが表示されてるのが残念な感じだが,簡単には消せないっぽいので気にしない方向で.
(ここは TextBox じゃなくて Label とかを使えばよいのかもしれないが.)

  • リスタートする手段としてメニューを設けた.
  • ゲームオーバー時とかの表示を真面目にやるのが面倒なので,下図のように簡素にステータスバーで済ませている.

Fig2.png

※なお,起動後にフォームのサイズをいい感じに調整することはユーザの仕事である.
(その辺を実装でどうにかしたいとか言い始めると,たいそう面倒なことになりそうだからパス)

Formのコード(50行くらい)
public partial class MainForm : Form
{
	private CharSTG.STG m_STG;	//移植物

	public MainForm(){	InitializeComponent();	}

	private void Restart()
	{
		Info_toolStripStatusLabel.Text = "";
		m_STG = new CharSTG.STG();
		UpdateView();
        GameLoop_timer.Start();
	}

	private void UpdateView()
	{
		MainView_textBox.Text = m_STG.ViewContent();
	}

	//-----------------------------------
	#region Event Handler
	
	//Form Load
	private void MainForm_Load(object sender, EventArgs e)
	{
		if( DesignMode )return;
		this.Text = "WinformsTest Ver.1.0.0";

		MainView_textBox.BackColor = Color.Black;
		MainView_textBox.ForeColor = Color.White;
		Restart();
	}

	//Reset Menu
	private void Reset_ToolStripMenuItem_Click(object sender, EventArgs e){	Restart();	}

	//Timer
	private void GameLoop_timer_Tick(object sender, EventArgs e)
	{
		if( m_STG.Update() )
		{	UpdateView();	}
		else
		{
			GameLoop_timer.Stop();
			if( m_STG.GameClear ){	Info_toolStripStatusLabel.Text = "** Game Clear !! **";	}
			else if( m_STG.GameOver ){	Info_toolStripStatusLabel.Text = "Game Over ...";	}
		}
	}

	#endregion
}

所感

たのしい.

この程度なら「使用するフレームワークに関する勉強」みたいなのはすごくミニマムと思うので,コンソールアプリより楽なんじゃないかな? という気もしてくる.

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?