DirectX無しでどこまでやれるか

タグの編集
投稿者 あにす  (社会人) 投稿日時 2008/11/18 00:43:10
60fpsで描画するゲームのメインループでDrawString()をして、
1フレームあたりのDrawString()の回数を増やしていってfpsの下がり具合を見てみました。
100  余裕の60fpsです。
500  まだ落ちません。60fps
1000 まだ60fpsを保ってます。
5000 26fps。一気に増やしすぎたようです。最低でも30fpsは欲しいと思いませんか?
4000 32fps。これくらいが滑らかに見える限界だと思います。

【環境】
OS WindowsXP Home Edition SP3
CPU AMD Sempron 3400+ 1.99GHz
RAM 1.96GB
処理系 VB2005EE

Imports System.Windows.Forms
Imports System.Drawing

Module Module1

    Sub Main()
        Dim pictureBox As New PictureBox()
        pictureBox.Dock = DockStyle.Fill
        Dim form As New Form()
        form.Controls.Add(pictureBox)
        form.Show()

        Dim bmp As New Bitmap(form.ClientSize.Width, form.ClientSize.Height)
        Dim g As Graphics = Graphics.FromImage(bmp)

        Dim timer As New System.Timers.Timer(1000)
        AddHandler timer.Elapsed, New System.Timers.ElapsedEventHandler(AddressOf timer_Elapsed)
        timer.Start()

        Dim nextFlame As Double = Environment.TickCount
        Dim wait As Single = 1000 / 60
        While form.Created
            If Environment.TickCount >= nextFlame Then
                If Environment.TickCount < nextFlame + wait Then
                    g.FillRectangle(Brushes.Black, 0, 0, pictureBox.Width, pictureBox.Height)
                    For i As Integer = 1 To 4000
                        g.DrawString(fps.ToString(), form.Font, Brushes.White, i * form.Font.Size, i * form.Font.Size)
                    Next
                    pictureBox.Image = bmp
                    drawCount += 1
                End If
                nextFlame += wait
            End If
            Application.DoEvents()
        End While
    End Sub

    Dim fps As Integer = 0
    Dim drawCount As Integer = 0
    Sub timer_Elapsed(ByVal sender As ObjectByVal e As System.Timers.ElapsedEventArgs)
        fps = drawCount
        drawCount = 0
    End Sub

End Module


僕もゲーム作りに挑戦したくなってきました。
投稿者 あにす  (社会人) 投稿日時 2008/11/19 00:01:53
挑戦しました。
シューティングゲームを作ろうと思い、カーソルキーでゲームのスタートや終了を選択できるタイトル画面から作り始めました。
カーソルキーの状態を取得しないといけませんが、VBにはキーボードの状態を取得する機能はないようです。
そこで、KeyUpイベントとKeyDownイベントでフラグを操作して、それを毎フレームごとに監視するようにしました。
常に一つ前のフレームの状態まで保持していれば、今押されているかどうかだけではなく、押し始めた瞬間も取得出来ると考えました。
しかし、↑、→、←、キーは取得できるのに↓キーだけ取りこぼしてしまいます。何故でしょう。
ミニマムコードを書いて実行してみると成功してしまいます。
もう少し悩んでみようと思います。
Imports System.Windows.Forms
Imports System.Drawing

Module Module1
    Sub Main()
        Dim frm As New Form()
        AddHandler frm.KeyDown, New KeyEventHandler(AddressOf frm_KeyDown)
        AddHandler frm.KeyUp, New KeyEventHandler(AddressOf frm_KeyUp)
        frm.Show()

        Dim g As Graphics = frm.CreateGraphics()

        Dim AKeyPushCount As Integer = 0
        Dim AKeyPressCount As Integer = 0

        Dim nextFlame As Double = Environment.TickCount
        Dim wait As Single = 1000 / 60

        While frm.Created
            If Environment.TickCount >= nextFlame Then
                upDate()
                If Push Then
                    AKeyPushCount += 1
                End If
                If Press Then
                    AKeyPressCount += 1
                End If
                If Environment.TickCount < nextFlame + wait Then
                    g.FillRectangle(Brushes.White, 0, 0, frm.ClientSize.Width, frm.ClientSize.Height)
                    g.DrawString("Push:" + AKeyPushCount.ToString() + " Press:" + AKeyPressCount.ToString(), frm.Font, Drawing.Brushes.Black, 0, 0)
                End If
                nextFlame += wait
            End If
            Application.DoEvents()
        End While
    End Sub

    Dim AKeyState() As Boolean = {False, False}
    Dim tmpAKeyState As Boolean = False

    ReadOnly Property Press() As Boolean
        Get
            Return AKeyState(1)
        End Get
    End Property

    ReadOnly Property Push() As Boolean
        Get
            Return AKeyState(1) AndAlso Not AKeyState(0)
        End Get
    End Property

    Sub frm_KeyDown(ByVal sender As ObjectByVal e As KeyEventArgs)
        If e.KeyData = Keys.Down Then
            tmpAKeyState = True
        End If
    End Sub

    Sub frm_KeyUp(ByVal sender As ObjectByVal e As KeyEventArgs)
        If e.KeyData = Keys.Down Then
            tmpAKeyState = False
        End If
    End Sub

    Sub upDate()
        AKeyState(0) = AKeyState(1)
        AKeyState(1) = tmpAKeyState
    End Sub
End Module
投稿者 茶封筒  (小学生) 投稿日時 2008/11/19 02:46:04
接触ルーチンは、どうされますか?
仮想マップは作れますか?
0123456789
10111213141516171819
20212223242526272829
以下略

投稿者 あにす  (社会人) 投稿日時 2008/11/19 05:06:35
接触ルーチンとは当たり判定でしょうか?
それなら、各オブジェクト(弾とか敵機とか)がこんな感じの
Public Interface IGameObject
    Sub Calc(ByVal objectList As List(Of IGameObject))'演算 
    Sub Draw(ByVal screenGraphics As Graphics)'描画 

    Property Location() As Rectangle'位置 
    Property Live() As Boolean'次のフレームまでに破棄される場合はFalse 
End Interface

インターフェースを実装するようにしているので、Rectangle.IntersectsWith()メソッドで
いいかなって思ってます。それでパフォーマンスが悪ければ自前で
a.Location.Left < b.Location.Right andalso a.Location.Right > b.Location.Left andalso a.Location.Top < b.Location.Bottom andalso THEN a.Location.Bottom > b.Location.Top
みたいな感じで比較するのがいいでしょうか。それでダメなら…
当たり判定用のBitmapも同時に描画するようにして、GetPixelするしか…

仮想マップとは座標の管理でしょうか?ユーザーが任意にスクロールしない仕様なので、
画面座標そのままで管理してます。
投稿者 うたひこ  (社会人) 投稿日時 2008/11/19 08:46:45
こんにちは。



僕が以前シューティング的アクション風ゲーム系アプリを作った時の工夫を書いてみます。



当たり判定をすべきオブジェクトに、敵弾、敵キャラ、味方弾、味方キャラがあり、当たり判定しないけど動きを持つオブジェクトとしてエフェクトがありました。

これらのオブジェクトは全て単一のクラスからインスタンス化したもので、かつそれらには、自身が敵か味方か、弾かキャラかを区別するプロパティを持たせませんでした。



では、どこでそれを判断したかというと、それらの管理、当たり判定、およびゲーム進行を統括する「フェイズ」クラスに、敵キャラ、味方キャラ、敵弾、味方弾、エフェクト、それぞれの属すべきコレクションを用意し、それで判断を行いました。

フェイズクラスは、コレクション対コレクションで当たり判定を行えばよく、わかりやすくて効率的です。



つまり、
「個体には、生まれ持っての正義か悪かの区別などなく、どこに属すかが、その者の善悪を定めた」
ということです。
投稿者 葉月  (社会人) 投稿日時 2008/11/19 15:05:17
>>>あにすさん
 どうも、始めまして。
 以前からお名前は拝見していましたが、やり取りはなかったと思います。
 以降、よろしくお願いします。

>カーソルキーの状態を取得しないといけませんが、VBにはキーボードの状態を取得する機能はないようです。
 押されたキーを判別する機能がありました。
 参考ページとサンプルコードを記載しますので、よかったらご確認ください。
 私が意味を捉え違いしてて、ご存知でしたら余計な手間をかけしてしまい申し訳ないです。
 
■参考ページ:
 http://msdn.microsoft.com/ja-jp/library/system.windows.forms.keys.aspx

■サンプルコード
    Private Sub Form1_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyDown

        Select Case e.KeyCode
            Case Keys.Up
                Console.WriteLine("上キー")
            Case Keys.Right
                Console.WriteLine("右キー")
            Case Keys.Left
                Console.WriteLine("左キー")
            Case Keys.Down
                Console.WriteLine("下キー")
        End Select

    End Sub
投稿者 あにす  (社会人) 投稿日時 2008/11/19 16:48:47
>うたひこさん
>これらのオブジェクトは全て単一のクラスからインスタンス化したもの
ということは、オブジェクト固有の振る舞いはどのように表現したのでしょうか?
僕は敵クラス、味方クラス、弾クラスetc...の必要なクラスを継承して、
そこにIGameObjectインターフェースを実装する形を考えています。
オブジェクトの敵味方等の判定は
If TypeOf obj Is 敵 Andalso TypeOf obj Is 弾 Then
    objは敵の弾
End If

のような形での判定をしています。このTypeOf演算子の演算コストがどれくらいかわからないので
オブジェクトの数が増えたときに心配ではあります。

>葉月さん
こちらこそよろしくお願いします。
確かにKeyEventArgsでキーの状態の取得は出来るのですが、
問題はKeyEventArgsがKeyDown,KeyUpイベント発生時にしか利用出来ないことです。
通常のウィンドウズアプリなら、このイベントが.NETフレームワークが自動で生成しているメッセージループから発生するので問題ないのですが、
自前でループを回してメッセージを処理するゲームの場合だと、任意のタイミングでキーボードの状態を取得出来ないと困ってしまいます。
そこで先のコードを思い付いたのですが…上手く行かなくて困ってしまいました。
投稿者 あにす  (社会人) 投稿日時 2008/11/19 17:07:55
連続での投稿失礼します。
実はゲームの開発は初めてなので、開発が進むと敵のAIの実装で悩むのではないかと思うのです。
そのときの試行錯誤のために、各オブジェクトの振る舞いを出来る限りカプセル化しておきたいという意図があり、一つのオブジェクトがゲーム全体を管理するという構造は取っていませんでした。
基本的な構造はこんな感じになってます
Imports System.Windows.Forms
Imports System.Drawing

Module Main
    <STAThread()> _
    Sub Main()
        '▼メインフォーム作成 
        Dim pb As New PictureBox()
        pb.Dock = DockStyle.Fill
        Dim mainForm As New Form()
        mainForm.KeyPreview = True
        mainForm.Controls.Add(pb)
        '▲メインフォーム作成 

        Dim screen As New Bitmap(mainForm.ClientSize.Width, mainForm.ClientSize.Height) 'ゲーム画面 
        Dim screenGraphics As Graphics = Graphics.FromImage(screen) 'ゲーム画面描画用 
        Dim objectList As New List(Of IGameObject)() 'ゲーム中のオブジェクト 

        'ここでゲーム開始初期のオブジェクトを追加 
        'ゲーム開始後はIGameObject.Calc()の中で追加や削除を行う 
        '描画のプライオリティ順に追加するようにする 
        objectList.Add(New タイトル画面の背景())
        objectList.Add(New カーソル())
        objectList.Add(New 選択肢ゲームスタート())
        objectList.Add(New 選択肢終了())

        '▼メインループ 
        Dim nextFrame As Double = Environment.TickCount
        Dim wait As Single = 1000 / 60
        While mainForm.Created
            If Environment.TickCount >= nextFrame Then
                '▼演算ループ 
                Dim i As Integer = 0
                Do
                    If objectList.Count > i Then
                        If objectList(i).Live Then
                            objectList(i).Calc(objectList)
                        Else
                            objectList.RemoveAt(i)
                            i -= 1
                        End If
                    Else
                        Exit Do
                    End If
                    i += 1
                Loop
                '▲演算ループ 
                If Environment.TickCount < nextFrame + wait Then
                    '▼描画ループ 
                    For Each obj As IGameObject In objectList
                        obj.Draw(screenGraphics)
                    Next
                    '▲描画ループ 
                    pb.Image = screen
                End If
                nextFrame += wait
            End If
            Application.DoEvents()
        End While
        '▲メインループ 
    End Sub
End Module
投稿者 うたひこ  (社会人) 投稿日時 2008/11/19 22:26:57
僕は「VBでゲームつくっちゃる」みたいな名前のサイト様をかなり参考にしてました。
vb6ですが。

>これらのオブジェクトは全て単一のクラスからインスタンス化したもの
これは、

>実はゲームの開発は初めてなので、開発が進むと敵のAIの実装で悩むのではないかと思うのです。
そのときの試行錯誤のために、各オブジェクトの振る舞いを出来る限りカプセル化しておきたいという意図があり、一つのオブジェクトがゲーム全体を管理するという構造は取っていませんでした。

を突き詰めるとそうなります。



同じ動きをするオブジェクトがあったとします。

例えば、敵弾、味方弾、敵キャラが別のキャラにホーミングするという動作。

これをポリモーフで実現しようとすると、
似たクラスを三つ作るというバカを丸出す羽目になります。

だったら別のクラスに切り離しちゃえ。



攻撃パターン(武器)が同じキャラが敵、味方にいたら?
武器も切り離しちゃえ。

アニメーションパターンがおんなじ。切り離しちゃえ。

と、クラスの分離を進めるうちに、
「敵、味方、キャラ、弾、エフェクトの区別」をも切り離そうとなったわけです。

そうなった時の最善の策として、「どこに属すか」による区別になりました。



自分が操作するキャラクターでさえ、
「キー入力を受け取るAIを持つキャラクター」として、
他のキャラと同じクラスに汎化されました。

この仕組みができた時はかなり気持ちよかったです(^^)
投稿者 あにす  (社会人) 投稿日時 2008/11/19 23:58:44
試しにキーの状態の取得部分をDirectInputを使用したコードに差し替えて見ても状況が改善されなかったので、
同じアルゴリズムでデータ構造を変えたコードを書きなおしました。なぜか上手く動くようになりました。
せっかくなのでコピペして使える様にモジュールをうp
Imports System.Windows.Forms

Module KeysState

    Dim keysState As New Dictionary(Of Keys, List(Of Boolean))

    ''' <summary> 
    ''' 毎フレーム呼び出す 
    ''' </summary> 
    Public Sub UpDate()
        For Each key As Keys In keysState.Keys
            keysState(key).RemoveAt(1)
            keysState(key).Add(keysState(key)(0))
        Next
    End Sub

    ''' <summary> 
    ''' キーが押し始められたかを取得 
    ''' </summary> 
    ''' <param name="key">押し始められたかどうかを取得するキー</param> 
    ''' <returns>押し始められていたらTrue、押し始められていなければFalse</returns> 
    Public Function IsPush(ByVal key As Keys) As Boolean
        Return keysState.ContainsKey(key) AndAlso Not keysState(key)(1) AndAlso keysState(key)(2)
    End Function

    ''' <summary> 
    ''' キーが押されているか取得 
    ''' </summary> 
    ''' <param name="key">押されているかどうかを取得するキー</param> 
    ''' <returns>押されていたらTrue、押されていなければFalse</returns> 
    Public Function IsPress(ByVal key As Keys) As Boolean
        Return keysState(key)(0)
    End Function

    ''' <summary> 
    ''' キーが離されたかを取得 
    ''' </summary> 
    ''' <param name="key">離されたかどうかを取得するキー</param> 
    ''' <returns>離されたらTrue、離されていなければFalse</returns> 
    Public Function IsUp(ByVal key As Keys) As Boolean
        Return keysState.ContainsKey(key) AndAlso keysState(key)(1) AndAlso Not keysState(key)(2)
    End Function

    ''' <summary> 
    ''' 対象のコントロールのKeyDownイベントハンドラに追加する 
    ''' </summary> 
    Public Sub KeyDown(ByVal sender As ObjectByVal e As KeyEventArgs)
        If keysState.ContainsKey(e.KeyCode) Then
            keysState(e.KeyCode)(0) = True
        Else
            keysState.Add(e.KeyCode, New List(Of Boolean)(New Boolean() {True, False, False}))
        End If
    End Sub

    ''' <summary> 
    ''' 対象のコントロールのKeyUpイベントハンドラに追加する 
    ''' </summary> 
    Public Sub KeyUp(ByVal sender As ObjectByVal e As KeyEventArgs)
        If keysState.ContainsKey(e.KeyCode) Then
            keysState(e.KeyCode)(0) = False
        Else
            keysState.Add(e.KeyCode, New List(Of Boolean)(New Boolean() {False, False, False}))
        End If
    End Sub
End Module


>うたひこさん
各オブジェクトの似ている要素を取り出してクラスにまとめるのは分かるのですが、
オブジェクトごとの固有の振る舞いはどこにコーディングしたのでしょうか?
違うコレクションに入れていても、同じクラスのインスタンスなら同じ動きしかしませんよね?
インスタンスのデータ部分で振る舞いを決定するにしても限界があるように思います。

>同じ動きをするオブジェクトがあったとします。
>
>例えば、敵弾、味方弾、敵キャラが別のキャラにホーミングするという動作。
>
>これをポリモーフで実現しようとすると、
>似たクラスを三つ作るというバカを丸出す羽目になります。
これを多態で表現するなら、『別のキャラにホーミングするクラス』を継承した
敵弾クラス、味方弾クラス、敵キャラクラスが出来ると思います。
そうすると、この3つのクラスは別のキャラにホーミングする以外の振る舞いのみが記述された全く違うクラスになりませんか?
あれ、VB6って継承出来ないんですね…。え、じゃぁどうやって多態を表現するんでしょう。わけが分からなくなって来ました…。
投稿者 うたひこ  (社会人) 投稿日時 2008/11/20 04:20:21
伝わらない文章ですみません。

>僕は「VBでゲームつくっちゃる」みたいな名前のサイト様をかなり参考にしてました。
>vb6ですが。
「僕はVB2005ユーザーで、紹介したサイト様はVB6ユーザー向けに書いてありますが、
それでも役に立つサイトなのでもしよろしければ」
といったつもりでした。



言葉で説明するのは大変そうなので、
今までのレスで言ってきたつもりのこと(&役立ちそうなアイディア)
をコードにしましたので、
動作はしませんが、アイディアとして参考にしてみてください。


'myを接頭語とした内部メンバと対応するように 
'外部公開用プロパティがあると仮定してご覧ください 

'キャラ、弾、エフェクト、アイテム 
Class Charactor

'メンバ 
Private mySpeed As Integer
Private myGrip As Integer
Private myHP As Integer
Private myLifeSpan As Integer = -1
Private myLocation As Point

Private myAI As IAI
Private myAnimation As IAnimation
Private myDeathEffect As IEffect
Private myDamagedEffect As IEffect
Private myWeapon As IWeapon


Overridable ReadOnly Property IsDied() As Boolean
Get
Return (myHP <= 0) Or (myLifeSpan = 0) 
End Get
End Property


Sub Calc(ByVal Data As IBattleData)
With Me
If Not .myAI Is Nothing Then .myAI.Calc(Me,Data)
If Not .myAnimation Is Nothing Then .myAnimation.Calc(Me,Data)
If Not .myWeapon Is Nothing Then .myWeapon.Calc(Me,Data)
End With
If myLifeSpan > 0 Then myLifeSpan -= 1
End Sub


Sub Draw(ByVal g As Graphics)
'割愛 
End Sub
'以降割愛 
End Class

Interface IAI
Sub Calc(ByVal Sender As Charactor, ByVal Data As IBattleData)
'以降割愛 
End Interface

Interface IAnimation
Sub Calc(ByVal Sender As Charactor, ByVal Data As IBattleData)
Sub Draw(ByVal g As Graphics)
'以降割愛 
End Interface

Interface IEffect
Sub Invoke(ByVal Sender As Charactor, ByVal Data As IBattleData)
'以降割愛 
End Interface

Interface IWeapon
Sub Calc(ByVal Sender As Charactor,ByVal Data As IBattleData)
'以降割愛 
End Interface



Interface IPhase

ReadOnly Property Keys As ReadOnlyCollection(Of System.Windows.Forms.Keys)
ReadOnly Property Random As Random

Sub Calc()
Sub Draw(ByVal g As Graphics)

Sub KeyDown(ByVal sender As Object , e As KeyEventArges)
Sub KeyUp(ByVal sender As Object , e As KeyEventArges)
'以降割愛 
End Interface

Interface IBattlePhase
Inherits IPhase
ReadOnly Property Enemies As List(Of Charactor)
ReadOnly Property EnemiesAtacks As 同上
ReadOnly Property Players As 同上
ReadOnly Property PlayersAtacks As 同上
ReadOnly Property BackEffects As 同上
ReadOnly Property FrontEffects As 同上
'以降割愛 
End Interface


'読み取りデータとしてCalcが行われている最中に 
'全てのオブジェクトが閲覧できるデータ。 

'オブジェクトの追加は、列挙操作中のコレクションの変更を回避することを目的とし 
'このインターフェイス内のAddを用いる。 

'このインターフェイスを実装するクラスは 
'Calcが終了した後で、Addで追加されたオブジェクトを 
'実際のコレクションに反映する。 
Interface IBattleData

ReadOnly Property Keys As ReadOnlyCollection(Of System.Windows.Forms.Keys)
ReadOnly Property Random As Random

ReadOnly Property Enemies As ReadOnlyCollection(Of Charactor)
ReadOnly Property EnemiesAtacks As 同上
ReadOnly Property Players As 同上
ReadOnly Property PlayersAtacks 同上
ReadOnly Property BackEffects As 同上
ReadOnly Property FrontEffects As 同上

Sub AddEnemy (ByVal Enemy As Charactor)
Sub AddPlayer(同上)
Sub AddEnemiesAtack (同上)
Sub AddPlayersAtack (同上)
Sub AddBackEffect (同上)
Sub AddFrontEffect (同上)

Sub RemoveEnemy(ByVal Enemy As Charactor)
'以降リムーブもAddと同じように続く。 
'実際は、Add、Remove、ReadOnlyCollectionをまとめた 
'コレクションを自作する。 
'このコードは説明用です。 

'以降割愛 
End Interface


Class BattlePhase
Implements IBattlePhase
Implements IBattleData
'以降割愛 
End Class

Class HomingAction
Implements IAI
'以降割愛 
End Class

Class UserAction
Implements IAI
'以降割愛 
End Class

Module BattleFactory
Function Stage1() As IBattlePhase
Dim Result As New BattlePhase

'主人公 
Dim Player As New Charactor
With Player
.HP = 200
.AI = New UserAction
End With

'敵 
Dim SampleEnemy As New Charactor
With SampleEnemy
.HP = 10
.AI = New HomingAction
End With

With Result
.Players.Add(Player)
.Enemies.Add(SampleEnemy)
End With

Return Result
End Function
End Module
投稿者 うたひこ  (社会人) 投稿日時 2008/11/20 05:11:15
説明不足っぽいかな・・・

Gofデザインパターンでいうとストラテジーで、
Charactorクラスをゲーム機とするならば、IAIはゲームソフトです。

Charactorクラスは単体では動いたりすることはできませんが、
「動き」のアルゴリズムの書かれたIAIをセットすることで動き出します。



UserActionには
「ユーザーからの入力を受け付ける」というアルゴリズムを実装し、
それを「動きの一つ」として考えます。

HomingActionも同様に
「対象に向かって進む」というアルゴリズムを
「動きの一つ」として考えます。



「動き」をオブジェクトとして捉え、
継承に依らずして振る舞いを交換できる仕組みの例です。
投稿者 あにす  (社会人) 投稿日時 2008/11/20 05:53:49
>うたひこさん
わざわざコードまで書いて下さりありがとうございます。
>Gofデザインパターンでいうとストラテジーで、
とてもすっきりしました。
ぜひ、うたひこさんのゲームで遊んでみたいです。

さて、キー入力の取得が上手く出来たので、画面に自機と1機の敵機を出して、自機が弾を発射して敵機を一撃で消し去るところまで出来ました。
しかし、敵が動きません。弾も出しません。この敵のAIを組んで完成にしたいのです。
敵のAIにさせたいことは
・移動して弾を出して自機を攻撃する
・他の敵を呼び出す
の2点です。これでゲームとして一応の形になり、DirectX無しでもゲームが作れることになると思います。
いま、必死で"シューティングゲーム AI"等のキーワードで検索したり2ちゃんねるのゲ制作板を見たりしてます。
投稿者 茶封筒  (小学生) 投稿日時 2008/11/20 06:39:06
敵機は、15機くらいでるのかなぁ~(^^)
投稿者 葉月  (社会人) 投稿日時 2008/11/20 07:10:49
>>>あにすさん
 イベント時にしか使えませんね。
 お手をわずらわせてしまってすいません。
 自前のキーコントロールがうまくいき、問題点が解決したようでよかったです。
 ゲーム製作も架橋のようで何よりです。
 応援しています。
投稿者 あにす  (社会人) 投稿日時 2008/11/20 08:25:34
とりあえず敵のAIは
"自機に突っ込んで発射"
"適当に増援を呼ぶ"
で実装しました。AIでもなんでもありませんね。
遊べるゲームを作る気はさらさらないので、これでいいのかも知れません。

>茶封筒さん
はい、それくらい出ます。しかし、それ以上出るとゲームが止まります。
毎フレーム全オブジェクトで当たり判定をしているのが原因のようです。

>葉月
いえいえ、応援されるようなものではないのです。好奇心で始めた実験のようなものですから。

とりあえず動くものを作ってみてわかりました。
DirectX云々以前にゲーム制作に関するノウハウがないと無理ですね。
描画以外に、処理コストを下げる工夫が必要な要素が山積みです。
1/60秒という時間の前でのCPUと自分の無力さを痛感しました。
これ以上の深入りはやめようと思います。

【反省点】
・当たり判定の頻度を最適化するべきだったかも知れない。
・オブジェクトは必要になったときにNewしていたが、先に充分な数のインスタンスを配列に持っておいて 際利用すれば良かったかも知れない。1/60秒でオブジェクトがNewされ続けるとGCも当てにならないのか も。
・自機も敵機もただの長方形、弾は丸。でも見た目は大事。ゲーム作るなら絵書きさんを連れてくるべき。
投稿者 茶封筒  (小学生) 投稿日時 2008/11/20 09:11:45
keyイベントはゼンゼン使えないが(反応なし)
セキュリィティソフトの関係かな?VB2008無料版です。
ゲームはキーボードを使える。(^^)
投稿者 うたひこ  (社会人) 投稿日時 2008/11/20 10:05:45
>キーを受け付けない

おそらくはFormのKEYpreviewプロパティがfalseのままになってるからでしょうね。

反映してるよ、ソコ。

KEYpreviewをTRUEにして反映してください、ソコ。



>あにすさん
僕がゲームを作った時の目的も、オブジェクトモデリングの修業とグラフィック描画の効率化がメインの目的だったので、ゲームとしての完成度はテキトーでした。

以来、設計系の話題に興味があるのですが、文章では表現しにくいので、なかなか取り沙汰されない中、今日はとても楽しかったです。

ありがとうございました。
投稿者 あにす  (社会人) 投稿日時 2008/11/20 13:07:43
>うたひこさん
僕も設計についてはとても興味があります。アプリの開発がある程度進んでくると、大規模なリファクタリングが始まります。必ずです。僕のプログラミングの恒例行事です。
そこで思うのです、事前に設計が出来ていればこの手間はかからないのに。と…。
UMLとデザインパターンに興味があるのですが、なかなかモノに出来ないでいます。
投稿者 あにす  (社会人) 投稿日時 2008/11/20 19:04:50
葉月さんが応援して下さったのに半端に終わらせちゃったので、遊べるゲームのコードをサクッと書きました。
VBだと慣れていなくて時間がかかっちゃうのでC#ですが、そのかわりコマンドラインでコンパイル出来るように書いてます。
VB2008がインストールされていればC#3.0のコンパイラも入ってるはずですよね…たぶん。
投稿者 あにす  (社会人) 投稿日時 2008/11/20 19:07:09
前半
/*
csc /target:winexe Form1.cs
*/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsApplication1 {
    public partial class Form1 :Form {
        const int cellSize = 70;//ブロックのサイズ
        int[,] table = new int[4, 4];//盤面
        Point selectCell = new Point(0, 0);//選択されているブロック
        PictureBox pictureBox1 = new PictureBox();//盤面を描画するピクチャーボックス

        [STAThread]
        static void Main() {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }

        public Form1() {
            pictureBox1.Dock = DockStyle.Fill;
            pictureBox1.Paint +=new PaintEventHandler(pictureBox1_Paint);
            pictureBox1.MouseMove += new MouseEventHandler(pictureBox1_MouseMove);
            pictureBox1.MouseDown += new MouseEventHandler(pictureBox1_MouseDown);
            this.FormBorderStyle = FormBorderStyle.FixedSingle;
            this.ClientSize = new Size(cellSize * 4 + 1, cellSize * 4 + 1);
            this.KeyPreview = true;
            this.Text = "15パズル";
            this.Load +=new EventHandler(Form1_Load);
            this.KeyDown += new KeyEventHandler(Form1_KeyDown);
            this.Controls.Add(pictureBox1);
        }

        private void Form1_Load(object sender, EventArgs e) {
            //1~15のリストを作りランダムにソート
            //盤面の左上から順番に値をセット
            Random rnd = new Random();
            do {
                List<int> list = new List<int>();
                for(int i = 1; i <= 15; i++) {
                    list.Add(i);
                }
                list.Sort(
                    (a, b) => {
                        if(a == b) return 0;
                        return rnd.Next(0, 2) == 0 ? -1 : 1;
                    }
                    );

                for(int y = 0; y < 4; y++) {
                    for(int x = 0; x < 4; x++) {
                        table[x, y] = list[list.Count - 1];
                        list.RemoveAt(list.Count - 1);
                        if(list.Count == 0) break;
                    }
                }
            } while(!verification());
            //解ける盤面になっているかをチェックして、ダメなら作り直し
        }

        //ユーザーのキー入力を処理
        private void Form1_KeyDown(object sender, KeyEventArgs e) {
            switch(e.KeyCode) {
            case Keys.Up:
                if(selectCell.Y > 0) selectCell.Y--;
                break;
            case Keys.Down:
                if(selectCell.Y < 3) selectCell.Y++;
                break;
            case Keys.Right:
                if(selectCell.X < 3) selectCell.X++;
                break;
            case Keys.Left:
                if(selectCell.X > 0) selectCell.X--;
                break;
            case Keys.Enter:
            case Keys.Space:
                slide();
                break;
            }
            pictureBox1.Refresh();

            judge();//解けているかどうか判定
        }
投稿者 あにす  (社会人) 投稿日時 2008/11/20 19:09:06
中盤
        //解けているか判定して、解けていたら終了orリトライ
        private void judge() {
            int count = 1;
            bool judge = true;
            for(int y = 0; y < 4; y++) {
                bool isBreak = false;
                for(int x = 0; x < 4; x++) {
                    if(table[x, y] != count) judge = false;
                    count++;
                    if(count >= 16) {
                        isBreak = true;
                        break;
                    }
                }
                if(isBreak) break;
            }
            if(judge) {
                if(MessageBox.Show("やたー!\nリトライしますか?","15パズル",MessageBoxButtons.YesNo) == DialogResult.Yes) {
                    Form1_Load(this, EventArgs.Empty);
                    pictureBox1.Refresh();
                } else {
                    this.Close();
                }
            }
        }

        //盤面が解けるパターンになっているかを検証
        //【参考URL】
        //http://hp.vector.co.jp/authors/VA010128/math/puzzle/P15-1.html
        private bool verification() {
            List<int> list = new List<int>();
            int count = 0;
            for(int y = 0; y < 4; y++) {
                for(int x = 0; x < 4; x++) {
                    list.Add(table[x, y]);
                }
            }
            list.Remove(0);
            for(int i = 0; i < list.Count; i++) {
                for(int j = i; j < list.Count; j++) {
                    if(list[i] > list[j]) count++;
                }
            }
            return count % 2 == 0;
        }
投稿者 あにす  (社会人) 投稿日時 2008/11/20 19:09:45
後半
        //盤面の状態と選択されているブロックによってブロックを動かす
        private void slide() {
            Point direction = new Point(0, 0);
            Point zero = searchZoro();
            if(zero.X == selectCell.X) { //縦
                if(zero.Y > selectCell.Y) {
                    direction = new Point(0, -1);
                } else if(zero.Y < selectCell.Y) {
                    direction = new Point(0, 1);
                }
            } else if(zero.Y == selectCell.Y) {//横
                if(zero.X > selectCell.X) {
                    direction = new Point(-1, 0);
                } else if(zero.X < selectCell.X) {
                    direction = new Point(1, 0);
                }
            }
            slide(direction);
        }
        private void slide(Point direction) {
            if(direction == new Point(0, 0)) return;
            Point zero = searchZoro();
            while(true) {
                table[zero.X, zero.Y] = table[zero.X + direction.X, zero.Y + direction.Y];
                zero.Offset(direction);
                if(zero == selectCell) {
                    table[selectCell.X, selectCell.Y] = 0;
                    return;
                }
            }
        }

        //ブロックが無い場所を探す
        private Point searchZoro() {
            for(int y = 0; y < 4; y++) {
                for(int x = 0; x < 4; x++) {
                    if(table[x, y] == 0) {
                        return new Point(x, y);
                    }
                }
            }
            return new Point();
        }

        private void pictureBox1_Paint(object sender, PaintEventArgs e) {
            e.Graphics.FillRectangle(Brushes.Black, 0, 0, this.ClientSize.Width, this.ClientSize.Height);//背景
            for(int y = 0; y < 4; y++) {
                for(int x = 0; x < 4; x++) {
                    if(table[x, y] == 0) continue;
                    Rectangle rect = new Rectangle(x * cellSize, y * cellSize, cellSize, cellSize);//ブロックの矩形
                    e.Graphics.FillRectangle(Brushes.AliceBlue, rect);//ブロック
                    e.Graphics.DrawRectangle(Pens.Black, rect);//枠
                    e.Graphics.DrawString(table[x, y].ToString(), new Font(FontFamily.GenericSerif, 38), Brushes.Black, rect);//ブロックの数字
                }
            }
            e.Graphics.DrawRectangle(new Pen(Color.White, 2), selectCell.X * cellSize, selectCell.Y * cellSize, cellSize, cellSize);//カーソル
        }

        //マウスが指し示す位置を選択
        private void pictureBox1_MouseMove(object sender, MouseEventArgs e) {
            for(int y = 0; y < 4; y++) {
                for(int x = 0; x < 4; x++) {
                    Rectangle rect = new Rectangle(x * cellSize, y * cellSize, cellSize, cellSize);
                    if(rect.Contains(e.Location)) {
                        selectCell = new Point(x, y);
                        pictureBox1.Refresh();
                        return;
                    }
                }
            }
        }

        //クリックはEnter or Spaceキーと等価
        private void pictureBox1_MouseDown(object sender, MouseEventArgs e) {
            Form1_KeyDown(this, new KeyEventArgs(Keys.Enter));
        }
    }
}