|
| 1 | +# title: Pyxel app 11-obstacle-race |
| 2 | +# author: Takayuki Shimizukawa |
| 3 | +# desc: 障害物レース |
| 4 | +# site: https://github.com/shimizukawa/pyxel-app |
| 5 | +# license: MIT |
| 6 | +# version: 1.0 |
| 7 | +# |
| 8 | +# /// script |
| 9 | +# requires-python = ">=3.11" |
| 10 | +# dependencies = [ |
| 11 | +# "pyxel", |
| 12 | +# ] |
| 13 | +# /// |
| 14 | + |
| 15 | +import time |
| 16 | +import random |
| 17 | +import pyxel |
| 18 | + |
| 19 | +TRANSPARENT_COLOR = 2 |
| 20 | +TILE_FLOOR = (1, 0) |
| 21 | +WALL_TILE_X = 4 |
| 22 | + |
| 23 | +_height = 0 |
| 24 | +player = None |
| 25 | +is_loose = False |
| 26 | +# show_bb = False |
| 27 | +# is_pback = False |
| 28 | +is_gameover = False |
| 29 | + |
| 30 | + |
| 31 | +def get_tile(tile_x, tile_y): |
| 32 | + return pyxel.tilemaps[0].pget(tile_x, tile_y) |
| 33 | + |
| 34 | + |
| 35 | +def is_colliding(x, y, is_falling, use_loose=False): |
| 36 | + x1 = pyxel.floor(x) // 8 |
| 37 | + y1 = pyxel.floor(y) // 8 |
| 38 | + x2 = (pyxel.ceil(x) + 7) // 8 |
| 39 | + y2 = (pyxel.ceil(y) + 7) // 8 |
| 40 | + if use_loose: |
| 41 | + x1 = (pyxel.floor(x) + 4) // 8 |
| 42 | + x2 = (pyxel.ceil(x) + 3) // 8 |
| 43 | + |
| 44 | + for yi in range(y1, y2 + 1): |
| 45 | + for xi in range(x1, x2 + 1): |
| 46 | + if get_tile(xi, yi)[0] >= WALL_TILE_X: |
| 47 | + return True |
| 48 | + if use_loose: |
| 49 | + return False |
| 50 | + |
| 51 | + if is_falling and y % 8 == 1: |
| 52 | + for xi in range(x1, x2 + 1): |
| 53 | + if get_tile(xi, y1 + 1) == TILE_FLOOR: |
| 54 | + return True |
| 55 | + return False |
| 56 | + |
| 57 | + |
| 58 | +def push_back(x, y, dx, dy): |
| 59 | + for _ in range(pyxel.ceil(abs(dy))): |
| 60 | + step = max(-1, min(1, dy)) |
| 61 | + if dy > 0 and is_colliding(x, y + step, dy > 0): |
| 62 | + break |
| 63 | + elif dy < 0 and is_colliding(x, y + step, dy > 0, use_loose=is_loose): |
| 64 | + break |
| 65 | + y += step |
| 66 | + dy -= step |
| 67 | + for _ in range(pyxel.ceil(abs(dx))): |
| 68 | + step = max(-1, min(1, dx)) |
| 69 | + if is_colliding(x + step, y, dy > 0): |
| 70 | + break |
| 71 | + x += step |
| 72 | + dx -= step |
| 73 | + return x, y |
| 74 | + |
| 75 | + |
| 76 | +class Player: |
| 77 | + def __init__(self, x, y, img): |
| 78 | + self.img = img |
| 79 | + self.X = x |
| 80 | + self.x = x |
| 81 | + self.y = y |
| 82 | + self.dx = 0 |
| 83 | + self.dy = 0 |
| 84 | + self.direction = 1 |
| 85 | + self.is_falling = False |
| 86 | + self.frame_count = 0 |
| 87 | + |
| 88 | + def __repr__(self): |
| 89 | + return f"Player(x={self.x}, y={self.y}, dx={self.dx}, dy={self.dy})" |
| 90 | + |
| 91 | + def update(self): |
| 92 | + self.frame_count = pyxel.frame_count |
| 93 | + last_y = self.y |
| 94 | + if pyxel.btn(pyxel.KEY_LEFT): |
| 95 | + self.dx = -1 * (2 if pyxel.btn(pyxel.KEY_SHIFT) else 1) |
| 96 | + self.direction = -1 |
| 97 | + if pyxel.btn(pyxel.KEY_RIGHT): |
| 98 | + self.dx = 2 * (2 if pyxel.btn(pyxel.KEY_SHIFT) else 1) |
| 99 | + self.direction = 1 |
| 100 | + self.dy = min(self.dy + 1, 10) |
| 101 | + if pyxel.btnp(pyxel.KEY_SPACE): |
| 102 | + if self.dy == 10 and not self.is_falling: # 落下3で落ちていない状態 |
| 103 | + self.dy = -6 |
| 104 | + self.x, self.y = push_back(self.x, self.y, self.dx, self.dy) |
| 105 | + |
| 106 | + # 頭をぶつけたら上昇を止める |
| 107 | + if self.dy <= 0 and self.y == last_y: |
| 108 | + self.dy = 0 |
| 109 | + |
| 110 | + # looseモードでの、ブロックハマりからの押し戻し処理 |
| 111 | + # if is_pback and is_colliding(self.x, self.y, False): |
| 112 | + # shift_x = round(self.x / 8) * 8 - self.x # 近い方のタイルにずらす |
| 113 | + # shift_x = max(-1, min(1, shift_x)) # ずらす量を-1, 0, 1に制限 |
| 114 | + # # 全てのdxについて、スクロール内で、かつ、ぶつかっていないdxがあるか |
| 115 | + # for dx in (-4, 4): |
| 116 | + # if is_colliding(self.x + dx, self.y, False): |
| 117 | + # self.x += shift_x |
| 118 | + # break |
| 119 | + # else: |
| 120 | + # # ハマっているので右方向にずらす |
| 121 | + # self.x += 1 |
| 122 | + |
| 123 | + if self.y < 0: |
| 124 | + self.y = 0 |
| 125 | + self.dx = int(self.dx * 0.8) |
| 126 | + self.is_falling = self.y > last_y |
| 127 | + |
| 128 | + if self.y >= _height: |
| 129 | + game_over() |
| 130 | + |
| 131 | + def draw(self): |
| 132 | + u = (2 if self.is_falling else self.frame_count // 3 % 2) * 8 |
| 133 | + w = 8 if self.direction > 0 else -8 |
| 134 | + self.img.blt(self.X, self.y, 0, u, 24, w, 8, TRANSPARENT_COLOR) |
| 135 | + # if show_bb: |
| 136 | + # if is_loose: |
| 137 | + # self.img.trib( |
| 138 | + # self.x + 4, self.y, self.x, self.y + 7, self.x + 7, self.y + 7, 14 |
| 139 | + # ) |
| 140 | + # else: |
| 141 | + # self.img.rectb(self.x, self.y, 8, 8, 10) |
| 142 | + |
| 143 | + |
| 144 | +class Obstacle: |
| 145 | + def __init__(self, x, y, img): |
| 146 | + self.img = img |
| 147 | + self.x = x |
| 148 | + self.y = y |
| 149 | + self.direction = 1 |
| 150 | + self.frame_count = 0 |
| 151 | + |
| 152 | + def __repr__(self): |
| 153 | + return f"Obstacle(x={self.x}, y={self.y})" |
| 154 | + |
| 155 | + def update(self): |
| 156 | + self.frame_count = pyxel.frame_count |
| 157 | + |
| 158 | + def draw(self): |
| 159 | + u = (self.frame_count // 3 % 2) * 8 |
| 160 | + w = 8 if self.direction > 0 else -8 |
| 161 | + self.img.blt(self.x - player.x, self.y, 0, u, 32, w, 8, TRANSPARENT_COLOR) |
| 162 | + |
| 163 | + |
| 164 | +class App: |
| 165 | + def __init__(self, width, height): |
| 166 | + self.width = width |
| 167 | + self.height = height |
| 168 | + global _height |
| 169 | + _height = height |
| 170 | + self.img = pyxel.Image(width, height) |
| 171 | + pyxel.load("assets/11-obstacle-race.pyxres") |
| 172 | + |
| 173 | + # Change enemy spawn tiles invisible |
| 174 | + pyxel.images[0].rect(0, 8, 24, 8, TRANSPARENT_COLOR) |
| 175 | + |
| 176 | + global player |
| 177 | + player = Player(8, 30, self.img) |
| 178 | + |
| 179 | + x = 96 |
| 180 | + self.obstacles = [ |
| 181 | + Obstacle(x, 72, self.img) |
| 182 | + ] |
| 183 | + for i in range(100): |
| 184 | + x += random.randint(3, 15) * 8 |
| 185 | + self.obstacles.append(Obstacle(x, 72, self.img)) |
| 186 | + |
| 187 | + def update(self): |
| 188 | + # global is_loose, show_bb, is_pback |
| 189 | + # if pyxel.btnp(pyxel.KEY_1): |
| 190 | + # show_bb = not show_bb |
| 191 | + # elif pyxel.btnp(pyxel.KEY_2): |
| 192 | + # is_loose = not is_loose |
| 193 | + # elif pyxel.btnp(pyxel.KEY_3): |
| 194 | + # is_pback = not is_pback |
| 195 | + if pyxel.btnp(pyxel.KEY_4): |
| 196 | + game_over() |
| 197 | + player.update() |
| 198 | + for obs in self.obstacles: |
| 199 | + obs.update() |
| 200 | + # 障害物当たり判定 |
| 201 | + x, y = player.x + 8, player.y |
| 202 | + for obs in self.obstacles: |
| 203 | + # もしobsとplayerが4ピクセル以上重なっていたら衝突 |
| 204 | + if abs(obs.x - x) <= 4 and abs(obs.y - y) <= 4: |
| 205 | + game_over() |
| 206 | + |
| 207 | + |
| 208 | + def render(self): |
| 209 | + g = self.img |
| 210 | + g.cls(0) |
| 211 | + |
| 212 | + # Draw level |
| 213 | + g.bltm(0, 0, 0, player.x % 8, 0, 128, 128, TRANSPARENT_COLOR) |
| 214 | + g.text(1, 1, f"DISTANCE: {player.x/10:4.1f} m", 7) |
| 215 | + # g.text(1, 1, "1:BBox", 7 if show_bb else 5) |
| 216 | + # g.text(32, 1, "2:Loose", 7 if is_loose else 5) |
| 217 | + # g.text(68, 1, "3:PBack", 7 if is_pback else 5) |
| 218 | + g.text(104, 1, "4:RST", 5) |
| 219 | + |
| 220 | + # Draw characters |
| 221 | + for obs in self.obstacles: |
| 222 | + obs.draw() |
| 223 | + player.draw() |
| 224 | + return g |
| 225 | + |
| 226 | + |
| 227 | +def game_over(): |
| 228 | + time.sleep(5) |
| 229 | + player.x = 0 |
| 230 | + player.y = 30 |
| 231 | + player.dx = 0 |
| 232 | + player.dy = 0 |
| 233 | + global is_gameover |
| 234 | + is_gameover = True |
| 235 | + |
| 236 | + |
| 237 | +class ParentApp: |
| 238 | + def __init__(self): |
| 239 | + pyxel.init(128, 96, title="obstacle race") |
| 240 | + self.child = App(width=128, height=96) |
| 241 | + pyxel.run(self.update, self.draw) |
| 242 | + |
| 243 | + def update(self): |
| 244 | + self.child.update() |
| 245 | + |
| 246 | + def draw(self): |
| 247 | + g = self.child.render() |
| 248 | + pyxel.blt( |
| 249 | + (pyxel.width - g.width) // 2, |
| 250 | + (pyxel.height - g.height) // 2, |
| 251 | + g, |
| 252 | + 0, |
| 253 | + 0, |
| 254 | + g.width, |
| 255 | + g.height, |
| 256 | + ) |
| 257 | + |
| 258 | + |
| 259 | +if __name__ == "__main__": |
| 260 | + ParentApp() |
0 commit comments