概要
以前,C# コンソールアプリ 初歩の初歩とかいう話で,無理やりコンソールアプリでSTGとかやろうとして「操作性がクソすぎて無理ゲー」という結論に達したことがあったのだが,文字を表示できればよいだけならば TextBox 使うのでよくね? とか思った.なんか急に.
操作性がどうのいう話のほかにも,必要ならば複数個の表示域を設けたりするとかも楽だろうし,ちょっとしたメニューとかもあるだけで断然便利だ.
……というわけで,件のSTGをものすごく雑に Winforms に移植してみた.
方針
- 表示域 →
TextBoxを使う:-
Multiline,ReadOnlyをtrueにしたやつを用意して文字列表示先として使う. -
Fontには等幅なやつを設定しておく. -
ForeColor,BackColorはお好みで. - 何なら適当に
Dock.Fillとか設定しておく.
-
- ゲームループ(処理をループさせる手段) →
Timerで済ませれば良いよね - キー入力 →
System.Windows.Input.Keyboard.IsKeyDown()とかで.- イベントでやるのは面倒なので.
- これ使うのに
PresentationCore.dllとWindowsBase.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 側
何も説明する必要もないくらいに簡素.
ど真ん中に TextBox を配置し,あとは単にタイマイベントで前記移植コードを使うだけの実装をちょろっと書けば終了だ.
左上にキャレットが表示されてるのが残念な感じだが,簡単には消せないっぽいので気にしない方向で.
(ここは TextBox じゃなくて Label とかを使えばよいのかもしれないが.)
- リスタートする手段としてメニューを設けた.
- ゲームオーバー時とかの表示を真面目にやるのが面倒なので,下図のように簡素にステータスバーで済ませている.
※なお,起動後にフォームのサイズをいい感じに調整することはユーザの仕事である.
(その辺を実装でどうにかしたいとか言い始めると,たいそう面倒なことになりそうだからパス)
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
}
所感
たのしい.
この程度なら「使用するフレームワークに関する勉強」みたいなのはすごくミニマムと思うので,コンソールアプリより楽なんじゃないかな? という気もしてくる.

