CELL / PIXEL / CAMERA
画面とマップの座標を分ける。
「何列目・何行目」と画面の x・y は、同じ数字でも単位がちがいます。3つの短いPythonの例で、マップの表の読み方と、画面に映すときの計算を確かめます。
- 前提
- 変数・if・関数・タプル・文字列
- 環境
- Python 3.11以上。追加ライブラリー・Pyxel不要
- 教材
- 単独で動く三つのコンソール例、解説、テスト
100本のゲーム講座とは別の、共通の入門ページです。マス目は長方形・整数の番号だけを使い、この例では Pyxel のウィンドウやマウス操作は使いません。
実行する前に結果を予想し、そのまま動かしてみましょう。そのあと数字を1つ変えて比べます。State やタプルの読み方はPythonの状態を読む、画面をくり返し描く仕組みは入力・更新・描画にまとめています。
マス番号から、描く位置へ
列と行は個数、xとyはピクセル
ゲームの地図で (2,3) と言うとき、このページでは「列2・行3」のマス番号のことです。左上のマスが (0,0) です。一方、画面に四角を描くときの x・y は、ピクセル(画面の点)で数えます。同じ数字の組でも、どちらの単位かを決めずに混ぜると、位置がずれてしまいます。
1マスの大きさが TILE=24、地図を描き始める位置の余白が ORIGIN=(32,60) なら、列2の左端は 32+2×24=80、行3の上端は 60+3×24=132 です。この計算で出るのはマスの左上の位置で、真ん中ではありません。真ん中に描きたいときは、さらに TILE/2 ずつ足します。
Pyxel の画面では、右へ行くほど x が、下へ行くほど y が大きくなります。数学のグラフ(上へ行くほど y が大きい)とは逆なので、row+1 は1段下のマスです。この例は数字をターミナルに出すだけなので、ウィンドウを開く処理や Pyxel のインストールはまだいりません。
return ORIGIN[0] + col*TILE, ORIGIN[1] + row*TILE予想して、そのまま動かす
ZIPを展開したフォルダーで、Windows は py -3 01_tile_pixels.py、macOS / Linux は python3 01_tile_pixels.py と実行します。ウィンドウは開かず、ターミナルに次の結果が出ます。
cell: (2, 3)
pixel top-left: (80, 132)
next column: (104, 132)
next row: (80, 156)列や行の番号に、いきなり余白の32や60を足さないようにしましょう。先に1マスの大きさを掛けて、最後にピクセルで数えた余白を足します。
TILE だけを24から16に変えてみましょう。cell は (2,3) のままで、左上は (64,108)、となりの列は (80,108)、1つ下の行は (64,124) になります。確かめたら24に戻します。
単独で動く全文を見る
# MIT License
#
# Copyright (c) 2026 Tidepost contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Exception: assets/glyphs.json contains rasterized glyph data derived from
# Noto Sans CJK JP (SIL Open Font License 1.1). See assets/NOTO-LICENSE.txt.
# Pyxel is an external dependency under its own MIT license.
"""マスの番号を、画面で描き始めるピクセルへ。Pyxel不要。"""
TILE = 24
ORIGIN = (32, 60)
def tile_to_pixel(col, row):
return ORIGIN[0] + col*TILE, ORIGIN[1] + row*TILE
def main():
col, row = 2, 3
print("cell:", (col, row))
print("pixel top-left:", tile_to_pixel(col, row))
print("next column:", tile_to_pixel(col+1, row))
print("next row:", tile_to_pixel(col, row+1))
if __name__ == "__main__":
main()配列は行から、次に列を読む
ROWS[row][col]と範囲外の拒否
文字列を上から並べた ROWS は、3行・5列の長方形の地図です。最初の [] で行(横1列ぶんの文字列)を選び、次の [] でその中の列(1文字)を選びます。座標は (col,row) の順で受け取っても、読むときは ROWS[row][col] の順です。外側のリストが「行の並び」だからです。
G は列3・行1にあるので、ROWS[1][3] で読めます。番号は0から数えます。人が「3列目」と数えると、番号では2になるので、このページでは「番号」とよんで区別します。幅は len(ROWS[0])、高さは len(ROWS) です。
Python では、マイナスの番号も使えてしまいます。ROWS[1][-1] はエラーにならず、その行のいちばん右の # を読みます。地図の左へはみ出したつもりが右はしを読んでしまうと困るので、読む前に「0以上か」と「幅・高さより小さいか」を両方確かめます。
tile_at は、範囲の外なら None、範囲の中ならその文字を返します。#(壁)と None(範囲の外)は、別の意味です。この短い例では、どの行も同じ幅であることを前提にしていて、最初の assert でそれを確かめています。文字を使った壁の判定やプレイヤーの移動は、各ゲーム講座で加えます。
| 行 / 列 | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| 0 | # | # | # | # | # |
| 1 | # | . | . | G | # |
| 2 | # | # | # | # | # |
if not (0<=row<len(ROWS) and 0<=col<len(ROWS[0])):
return None
return ROWS[row][col]予想して、そのまま動かす
ZIPを展開したフォルダーで、Windows は py -3 02_map_bounds.py、macOS / Linux は python3 02_map_bounds.py と実行します。ウィンドウは開かず、ターミナルに次の結果が出ます。
width,height: (5, 3)
cell (0, 0) -> #
cell (3, 1) -> G
cell (-1, 1) -> None
cell (5, 1) -> None
raw negative index: #len(ROWS) を横の幅として使わないようにしましょう。この地図は幅5・高さ3なので、取りちがえると、列3にある G を「範囲の外」とまちがえてしまいます。
まん中の行の文字列を #..G# から #G..# に変えてみましょう。tile_at(1,1) は G、tile_at(3,1) は . になります。幅5・高さ3は変わりません。左右のはしの # は残したまま、確かめたら元に戻します。
単独で動く全文を見る
# MIT License
#
# Copyright (c) 2026 Tidepost contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Exception: assets/glyphs.json contains rasterized glyph data derived from
# Noto Sans CJK JP (SIL Open Font License 1.1). See assets/NOTO-LICENSE.txt.
# Pyxel is an external dependency under its own MIT license.
"""rows[row][col]で読む前に、二つの範囲を確認する。Pyxel不要。"""
ROWS = ("#####", "#..G#", "#####")
assert ROWS and all(len(row)==len(ROWS[0]) for row in ROWS)
def tile_at(col, row):
if not (0<=row<len(ROWS) and 0<=col<len(ROWS[0])):
return None
return ROWS[row][col]
def main():
print("width,height:", (len(ROWS[0]), len(ROWS)))
for cell in ((0,0), (3,1), (-1,1), (5,1)):
print("cell", cell, "->", tile_at(*cell))
print("raw negative index:", ROWS[1][-1])
if __name__ == "__main__":
main()世界の位置から、カメラのずれを引く
世界座標と画面座標の往復
広い地図の一部だけを画面に見せるときも、プレイヤーの「世界のマス番号」は変えません。CAMERA=(2,1) は画面の左上に見せる世界のマス、VIEW=(4,3) は画面に見せる列の数と行の数です。画面に出す前に、世界の座標から CAMERA を引いて、画面の中でのマス番号に直します。
世界の (5,3) は、画面の中では (3,2) です。1マス16、余白 (8,8) なら、画面の左上 (56,40) に描きます。世界の (6,3) は画面の中の列4になり、幅4の窓からはみ出すので None です。この関数は、図形の一部だけを切りぬいて見せるものではなく、マス単位で「画面に入るか」を調べるものです。
画面のピクセルからマスを求めるときは、逆の順番で計算します。余白を引き、//TILE でマス番号に切り捨て、CAMERA を足します。ピクセル (71,55) は窓の右下の最後のピクセルで、世界の (5,3) です。その1つ右の x72 は範囲の外です。右はしと下はしは「含めない」にそろえています。
左の余白にある x7 は None です。画面の外かどうかを先に調べずにマイナスの値を割ると、Python では -1//16 は -1 になります。int(-1/16) の0とはちがう結果です。この例では整数の座標だけを扱い、実際のマウスの入力や、カメラの追いかけ・拡大縮小は作っていません。
local_col, local_row = col-CAMERA[0], row-CAMERA[1]予想して、そのまま動かす
ZIPを展開したフォルダーで、Windows は py -3 03_camera_offset.py、macOS / Linux は python3 03_camera_offset.py と実行します。ウィンドウは開かず、ターミナルに次の結果が出ます。
world cell (5,3): (56, 40)
world cell (6,3): None
screen pixel (71,55): (5, 3)
screen pixel (72,55): None
left of view (7,8): Noneカメラの分を引いた位置で、プレイヤーの世界の座標を上書きしないようにしましょう。カメラは「どこを見せるか」を決めるもので、世界の中の本当の位置とは別のものです。
CAMERA を (2,1) から (3,2) に変えてみましょう。world_to_screen(5,3) は (40,24)、screen_to_world(40,24) は (5,3) になります。同じ世界の位置を、画面の別の場所に映せることが分かります。確かめたら (2,1) に戻します。
単独で動く全文を見る
# MIT License
#
# Copyright (c) 2026 Tidepost contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
# Exception: assets/glyphs.json contains rasterized glyph data derived from
# Noto Sans CJK JP (SIL Open Font License 1.1). See assets/NOTO-LICENSE.txt.
# Pyxel is an external dependency under its own MIT license.
"""世界のマス番号、表示範囲、画面のピクセルを分ける。Pyxel不要。"""
TILE = 16
ORIGIN = (8, 8)
VIEW = (4, 3)
CAMERA = (2, 1)
def world_to_screen(col, row):
local_col, local_row = col-CAMERA[0], row-CAMERA[1]
if not (0<=local_col<VIEW[0] and 0<=local_row<VIEW[1]):
return None
return ORIGIN[0]+local_col*TILE, ORIGIN[1]+local_row*TILE
def screen_to_world(x, y):
local_x, local_y = x-ORIGIN[0], y-ORIGIN[1]
if not (0<=local_x<VIEW[0]*TILE and 0<=local_y<VIEW[1]*TILE):
return None
return CAMERA[0]+local_x//TILE, CAMERA[1]+local_y//TILE
def main():
print("world cell (5,3):", world_to_screen(5,3))
print("world cell (6,3):", world_to_screen(6,3))
print("screen pixel (71,55):", screen_to_world(71,55))
print("screen pixel (72,55):", screen_to_world(72,55))
print("left of view (7,8):", screen_to_world(7,8))
if __name__ == "__main__":
main()ゲームのマップへ進む。
一歩貸してくださいではマス単位の移動を、後から来る壁では同じマスにあとで起きる地形の変化を読めます。見た目のピクセルの位置と、判定に使うマス番号がどこにあるか、探してみましょう。
確認したこと
Linux / Python 3.12.14で32テスト成功。幅4×高さ3マスの表示窓について、全3072整数ピクセルの逆変換、境界、三つのソース変更課題を照合しました。3本を改名して日本語・空白の独立フォルダーへ移し、UTF-8/CP932出力の計6起動で期待した表示と一致。CP932既定読み取りの模擬でも全テストが成功しています。
Python 3.11そのものの起動、Windows / macOS実機、ブラウザー、マウス機器、人の理解度は未確認です。このコンソール例の検査をPyxelゲームの描画検査へ加算しません。説明用の表は画面収録ではありません。
既存教材のMIT通知と参照されるOFL本文を保持。外部の字形データは使っていません。