【初心者向け】KivyによるWindowsアプリ作成21 リストを使用したWidget重複表示の防止

前回の記事では、ゲーム画面に1秒間隔で「a」という文字を表示するような実装を行いました。しかしながら、この実装には問題があり、乱数の生成状況によっては、文字が同じ場所に重なって表示されてしまうことがあるのでした。

そこで今回は、この重複表示を防止する方法について説明したいと思います。

コードはGitHubにアップしていますので、必要に応じてご参照下さい。

<スポンサーリンク>

対応方針の検討

ではまず、認識された課題に対して、どのように対応するか、方針を簡単に確認しておきましょう。

同じ場所に文字を表示させないようにするには、文字を表示させたタイミングでその位置を記憶しておき、次に文字を表示する時の位置指定の際、その記憶した場所以外の位置を指定するようにしてやればよさそうです。

そこで、本記事では、以下の手順で課題を解消するための実装を行います。

①表示対象領域である10 X 7のマスごとに、文字が表示済みかどうかを記憶しておく配列を作成

②文字表示位置として、この配列を参照して文字が存在しない位置から1つをランダムに選択

③その位置に文字を表示するとともに、配列に位置を記憶

実装

①文字表示位置を記憶する配列作成

最初に配列を作成しておきたいと思います。

配列は、横10マス、縦7マスの10 X 7の二次元配列とし、文字が既に表示されている場合にはTrueを、表示されていない場合はFalseを入れておくものとします。

とすると、配列を作成する際、すべての要素をFalseで初期化しておく必要がありますね。

GameScreenクラスのstartメソッドの中に、配列の作成と初期化を行う行を追加します。

def start(self):
    self.targetExist = [[False for i in range(10)] for j in range(7)] #この行を追加
    Clock.schedule_interval(self.update, 1.0/1.0)

追加しているのは1行だけですが、やや、ややこしいです。

順に説明します。

まず、「self.targetExist」という表現で、GameScreenクラス(self)内共通で使える変数として「targetExist」というものを宣言しています。単に「targetExist」とだけ書くと、startメソッド内だけで利用できる変数になってしまいますので、ご注意下さい。

次に等号の右側ですが、まずは内側の[False for i in range(10)] という表現についてです。

この中の「range(10)」という記述ですが、「range(n)」という表現で、0から連続したn個の整数の組み合わせを生成することができます。ですので、「range(10)」では、0から9までの10個の整数を生成します。

これを踏まえて全体ですが、[値 for 一時変数 in range(n)]という記述で

  • 0からnまでの連続した整数の組み合わせを生成し、
  • そこから整数を1つずつ「一時変数」に入れた上で、
  • 「値」を返すという作業を整数の個数(n個)分だけ実行し、
  • その結果をまとめてリスト形式で返す

 ということができます

ここで「リスト」とは配列の一種で、「要素の中身が変更可能な複数の要素の組み合わせ」です。例えば、[0, 1, 2]のようなものがリストです。

ちなみに配列には「タプル」という、リストとは異なり、要素の中身が変更ができないものや、「ディクショナリ」という、キーと値を1セットとし、それを複数持ったものがあります。

ディクショナリについては、PythonプログラムからKvファイル上のWidgetにアクセスにする際、「ids」という名称のものを利用しましたね。

前回までで経費申請アプリのログイン画面作成までが完了しました。 今回と次回の2回で、本アプリのメインとなる画面である、経費申請画面を作...

ところで今回の場合、「一時変数」は利用されていないのでは?と思われた方もいらっしゃるかと思います。実際、その通りで、単に既定の回数分処理を回すために上記の記述を使っていて、「一時変数」の値そのものは意味を持ちません。

ちなみに、当然ながら「値」には、「一時変数」を使った関数を使用することもできます。例えば以下のようなものです。

evenNumbers = [i*2 for i in range(10)]
print(evenNumbers)

これを実行すると、range(10)によって生成された0から9までの数が、それぞれ2倍された数字の列が表示されます。

[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

少し横道にそれましたが、結局、[False for i in range(10)] という記述で、[False, False, False, False, False, False, False, False, False, False]という、Falseが10個並んだリストが生成されます。

さらにこれを踏まえて、[[False for i in range(10)] for j in range(7)]という表現ですが、これにより、今説明した、Falseが10個並んだリストが7個含まれるリスト(リストのリスト)が生成されることになります。

したがって、追加した行全体では、この10 X 7のリストのリストを、GameScreenクラス内で共通的に使える変数targetExistに格納するという処理を行っています。

このリストのリストが、ゲーム画面の10行 X 7列のマス目に対応します。

②文字表示時位置のランダム選択

次に、文字を表示する位置を決める際に、先ほどの配列を参照して、文字がまだ表示されていない(=配列中の対応する要素の値がFalse)位置の中からランダムに1つを選択する処理を実装します。

まずは必要なクラスをインポートします。

前回インポートしたrandintの代わりにchoiceというものを用いますので、import文を以下のように書き換えます。

#省略
from kivy.uix.button import Button
from random import choice #randintをchoiceに変更
from kivy.clock import Clock
#省略

続いて、GameScreenクラス中のupdateメソッドに対し、以下のように追記して下さい。

def update(self, dt):
    indexList = [] #ここから追加
    for j in range(7):
        for i in range(10):
            if self.targetExist[j][i] == False:
                indexList.append([j,i])
                y, x = choice(indexList) #ここまで追加

    target = Target()
#省略

まず2行目で、「indexList」という空のリストを定義しています。このリストは3行目以降に追記した処理の中で、表示位置を記憶したTargetExistを一通り参照し、文字が表示されていないマス目だけを一時的に記憶しておくために使います。

3、4行目は、10 X 7のTargetExistを一通り参照するためのループ文です。

2段階になっていますが、イメージ的には「for j in range(7):」で10個のFalseからなるリスト7個のうち、先頭から1個ずつ取り出し、さらに、「for i in range(10):」で取り出された1個のリストの中から要素を1つずつ取り出すという処理を行います。

このループを回す中で、マス目の行に対応するjには0~6の整数が、列に対応するiには0~9の整数が順に入ることになり、この数字を用いて、先ほど作成したリストのリストであるtargetExistにおける要素に順にアクセスすることができます。

なお、先ほども「for」という記述が出てきましたが、そちらはリスト内包表記と呼ばれる、少し特殊な使い方をしています。スタンダードな使い方は、このようにfor文の下の行に繰り返し行いたい処理を記述するものです。

forのスタンダードな使い方については、以下の記事でも説明していますので、必要であればご参照下さい。

前回までで経費申請アプリのログイン画面作成までが完了しました。 今回と次回の2回で、本アプリのメインとなる画面である、経費申請画面を作...

さて、次の5,6行目ですが、表示有無を記憶したリストを、変数i, jを使って一通り参照し、要素がFalseの場合、そのiとjをindexListに格納するという処理を行っています。少しくどいかもしれませんが、jが行、iが列に対応しますので、indexListには[行, 列]の組み合わせが複数格納されることになります。

なお、今回のtargetExistのようなリストのリストにアクセスする場合、targetExist[インデックス1][インデックス2]という表現でアクセスしますが、前側のインデックス1には7個のリスト中の何番目かを指定、後ろ側のインデックスにはリスト中の10個の要素のうち何番目かを指定、というように、リストの外側からアクセスするインデックスを指定していくことに注意下さい。

また、リストのインデックスは1から開始するのではなく、0から開始されることにもご注意下さい。マス目で言うと、例えば5行目、2列目の要素であればtargetExist[4][1]で、1行目、10列目の要素であればtargetExist[0][9]でアクセスできることになります。

一部繰り返しになりますが、ここまでの記述で、indexListには文字がまだ表示されていないマス目の[行, 列]の組み合わせが一通り格納されることになります。

さらに、7行目の「choice(indexList)」でindexListの中からマス目1つをランダムに選択し、その行、列をそれぞれ、変数y, xに格納します。

これで、文字がまだ表示されていないマス目から1つを選択し、その位置を変数x, yに格納するという処理が実装できました。

③文字の表示とtargetExistリストへの位置記憶

最後に、取得した変数x, yを用いて実際に文字を表示するとともに、その位置x, yをtargetExistに記憶するよう、updateメソッドを修正しましょう。

def update(self, dt):
#省略
    target = Target()
    target.pos = (x * 75 + 26, y * 75 + 1) #変更
    target.text = "a"
    self.add_widget(target)
    self.targetExist[y][x] = True #追加

対応する箇所は2行あります。

まず4行目は、文字を表示する位置をtargetインスタンスの属性として指定する記述ですが、前回「target.pos = (randint(0, 9) * 75 + 26, randint(0, 6) * 75 + 1)」のように、x座標/y座標をrandintを使って指定していたところ、先ほど取得したx, yの値を使って指定するようにしています。

次に、7行目で、targetExistの中の、表示した位置に対応する要素をTrueとし、以後、文字の表示対象位置にしないようにしています。

これで、実装は一通り完了です。

プログラム実行

それでは、意図したとおりにプログラムが動くか、実行して確認してみましょう。

前回までのプログラムでは、重複表示が行われてしまったタイミングで、見た目が何も変化しない1秒間が発生してしまいますが、修正後のプログラムでは、1秒間隔で必ずどこかしらにか文字が新しく表示されるはずです。

なお、画面が文字で埋め尽くされた後は、ゲームが異常終了してしまいます。

これは、targetExistがすべてTrueとなった時、indexListに何も格納されないことになりますが、そこでindexListからマス目を取り出そうとして空振りすることによります。

この事象に対して、本プログラムでは別途、表示文字が一定の個数を超えた場合にゲームオーバーポップアップを表示する処理を追加することにより、画面が文字で埋め尽くされることが発生し得なくなりますので、そのまま放置しておくことにします。

終わりに

今回の記事では、リストの扱い方についての説明が主でしたが、私自身、過去にリストのイメージがなかなか沸かず苦戦した経験がありますので、くどいぐらいに説明してみました。分かりづらい箇所もあるかもしれませんが、テスト的に簡単なプログラムを書いてみたり、インターネット上の他の情報も検索したりしながら、理解を深めて頂ければと思います。

さて、次回は今回使用したchoice関数を応用して、表示する文字を「a」だけではなく、アルファベットの小文字全体からランダムに選択したものを表示する処理を実装したいと思います。

<スポンサーリンク>

シェアする

フォローする