Saturn Voyager


隕石を避けながら進めるだけ進むというシンプルなゲームです。3Dのように見えるかもしれませんが、実際には2Dの画像を描画しているだけの疑似3Dです。昔はこのような疑似3Dのゲームも少なくありませんでした。以下の書籍に詳しい解説があります。

<!DOCTYPE html>
<html>
<head>
    <title>SaturnVoyager</title> 
    <META charset="UTF-8">
    <style>
        #space {
            width: 800px; height: 800px;
            touch-action: none;
        }
        #START {
            position: absolute;
            left: 200px;
            top: 200px;
        }        
    </style>
    <script>
        "use strict";
        var stars = [], keymap = [];
        var ctx, ship, score = 0, speed = 25, timer = NaN;

        function Ship(x, y) {
            this.x = x;
            this.y = y;
            this.keydown = function (e) {
                keymap[e.keyCode] = true;
            }
            this.keyup = function (e) {
                keymap[e.keyCode] = false;
            }
            this.move = function () {
                if (keymap[37]) {           // 左
                    this.x -= 30;
                } else if (keymap[39]) {    // 右
                    this.x += 30;
                }

                if (keymap[38]) {           // 上
                    this.y -= 30;
                } else if (keymap[40]) {    // 下
                    this.y += 30;
                }
                this.x = Math.max(-800, Math.min(800, this.x));
                this.y = Math.max(-800, Math.min(800, this.y));
            }
        }

        function random(v) {
            return Math.floor(Math.random() * v);
        }


        function init() {
            for (var i = 0 ; i < 200 ; i++) {
                stars.push({
                    x: random(800 * 4) - 1600,
                    y: random(800 * 4) - 1600,
                    z: random(4095),
                    r: random(360),
                    w: random(10) - 5
                });
            }

            ship = new Ship(200, 200);
            onkeydown = ship.keydown;
            onkeyup = ship.keyup;

            var space = document.getElementById("space");
            ctx = space.getContext("2d");
            ctx.font = "20pt Arial";
            repaint();
        }

        function go() {
            var space = document.getElementById("space");
            space.onmousedown = mymousedown;
            space.onmouseup = mymouseup;
            space.oncontextmenu = function (e) { e.preventDefault(); };
            space.addEventListener('touchstart', mymousedown);
            space.addEventListener('touchend', mymouseup);   
                        
            document.body.addEventListener('touchmove', function (event) {
                event.preventDefault();
            }, false);
            document.getElementById("START").style.display = "none";
            document.getElementById("bgm").play();
            timer = setInterval(tick, 50);
        }

        function mymousedown(e) {                   
            var mouseX = (!isNaN(e.offsetX) ? e.offsetX : e.touches[0].clientX) - 400;
            var mouseY = (!isNaN(e.offsetY) ? e.offsetY : e.touches[0].clientY) - 400;
            if (Math.abs(mouseX) > Math.abs(mouseY)) {
                keymap[mouseX > 0 ? 37 : 39] = true;
            } else {
                keymap[mouseY > 0 ? 38 : 40] = true;
            }        
        }

        function mymouseup(e) {
            keymap = [];         
        }

        function tick() {
            for (var i = 0 ; i < 200 ; i++) {
                var star = stars[i];
                star.z -= speed;
                star.r += star.w;
                if (star.z < 64) {
                    if (Math.abs(star.x - ship.x) < 50 &&
                        Math.abs(star.y - ship.y) < 50) {
                        // 衝突→ゲームオーバー
                        clearInterval(timer);
                        timer = NaN;
                        document.getElementById("bgm").pause();
                        break;
                    }
                    // 通過→奥へ再配置
                    star.x = random(800 * 4) - 1600;
                    star.y = random(800 * 4) - 1600;
                    star.z = 4095;
                }
            }
            if (score++ % 10 == 0) {
                speed ++;
            }
            ship.move();
            repaint();
        }

        function repaint() {
            ctx.fillStyle = "black";
            ctx.fillRect(0, 0, 800, 800);
            stars.sort(function (a, b) {
                return b.z - a.z;
            });

            // 隕石の描画
            for (var i = 0 ; i < 200 ; i++) {
                var star = stars[i];
                var z = star.z;
                var x = ((star.x - ship.x) << 9) / z + 400;
                var y = ((star.y - ship.y) << 9) / z + 400;
                var size = (50 << 9) / z;
                ctx.save();
                ctx.translate(x, y);
                ctx.globalAlpha = 1- (z / 4096);
                ctx.rotate(star.r * Math.PI / 180);
                ctx.drawImage(rockImg, -size / 2, -size / 2, size, size);
                ctx.restore();
            }

            // スコア
            ctx.drawImage(scope, 0, 0, 800, 800);
            ctx.fillStyle = "green";
            ctx.fillText(('0000000' + score).slice(-7), 670, 60);
            if (isNaN(timer)) {
                ctx.fillText("GAME OVER", 315, 350);
            }
        }
    </script>
</head>
<body onload="init()">
    <!-- Thanks to http://takao-suenobu.com/  & http://dova-s.jp/ -->
    <audio src="Escape.mp3" id="bgm" loop="loop"></audio> 
    <canvas id="space" width="800" height="800"></canvas>
    <img id="START" src="start.png" onclick="go()"><br/>
    <img id="rockImg" src="rock.png" style="display:none" />
    <img id="scope" src="scope.png" style="display:none" />
</body>
</html>

""" saturn_voyager.py - Copyright 2016 Kenichiro Tanaka """
import sys
from random import randint
import pygame
from pygame.locals import QUIT, KEYDOWN, KEYUP, \
    K_LEFT, K_RIGHT, K_UP, K_DOWN

pygame.init()
SURFACE = pygame.display.set_mode((800, 800))
FPSCLOCK = pygame.time.Clock()

def main():
    """ メインルーチン """
    game_over = False
    score = 0
    speed = 25
    stars = []
    keymap = []
    ship = [0, 0]
    scope_image = pygame.image.load("scope.png")
    rock_image = pygame.image.load("rock.png")

    scorefont = pygame.font.SysFont(None, 36)
    sysfont = pygame.font.SysFont(None, 72)
    message_over = sysfont.render("GAME OVER!!",\
                                        True, (0, 255, 225))
    message_rect = message_over.get_rect()
    message_rect.center = (400, 400)

    while len(stars) < 200:
        stars.append({
            "pos": [randint(-1600, 1600),
                    randint(-1600, 1600), randint(0, 4095)],
            "theta": randint(0, 360)
        })

    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN:
                if not event.key in keymap:
                    keymap.append(event.key)
            elif event.type == KEYUP:
                keymap.remove(event.key)

        # フレーム毎の処理
        if not game_over:
            score += 1
            if score % 10 == 0:
                speed += 1

            if K_LEFT in keymap:
                ship[0] -= 30
            elif K_RIGHT in keymap:
                ship[0] += 30
            elif K_UP in keymap:
                ship[1] -= 30
            elif K_DOWN in keymap:
                ship[1] += 30

            ship[0] = max(-800, min(800, ship[0]))
            ship[1] = max(-800, min(800, ship[1]))

            for star in stars:
                star["pos"][2] -= speed
                if star["pos"][2] < 64:
                    if abs(star["pos"][0] - ship[0]) < 50 and \
                        abs(star["pos"][1] - ship[1]) < 50:
                        game_over = True
                    star["pos"] = [randint(-1600, 1600),
                                   randint(-1600, 1600), 4095]

        # 描画
        SURFACE.fill((0, 0, 0))
        stars = sorted(stars, key=lambda x: x["pos"][2],
                       reverse=True)
        for star in stars:
            zpos = star["pos"][2]
            xpos = ((star["pos"][0] - ship[0]) << 9) / zpos + 400
            ypos = ((star["pos"][1] - ship[1]) << 9) / zpos + 400
            size = (50 << 9) / zpos
            rotated = pygame.transform.rotozoom(rock_image,
                                    star["theta"], size / 145)
            SURFACE.blit(rotated, (xpos, ypos))

        SURFACE.blit(scope_image, (0, 0))

        if game_over:
            SURFACE.blit(message_over, message_rect)
            pygame.mixer.music.stop()

        # スコアの描画
        score_str = str(score).zfill(6)
        score_image = scorefont.render(score_str, True,
                                       (0, 255, 0))
        SURFACE.blit(score_image, (700, 50))

        pygame.display.update()
        FPSCLOCK.tick(20)

if __name__ == '__main__':
    main()