Ark's Blog

引っ越しました→ https://blog.arkark.dev/

ようこそ

ゲーム制作に活かす座標系の話 | TokyoTech traP LT#3

1/17(水)にTokyotechTrap LT#3というLTがありました。 発表したのでそのときのスライドとか補足とかです。

スライド

デモ

デモはこちらです。

ちゃんと作ってないのでバグがあるかも。 p5.jsを使ってます。

ソースコード

"use strict";

const p5 = require("p5");

const sketch = (p) => {

    const CANVAS_WIDTH  = 500;
    const CANVAS_HEIGHT = 500;
    const HEX_SIZE = 20;
    const CELL_NUM = 5;

    const label = document.querySelector("#label");

    function iota(n) {
        return Array.from({length: n}, (_, i) => i);
    }

    function hexToCartesian(hex) {
        return {
            x: HEX_SIZE * 3/2 * hex.q,
            y: HEX_SIZE * p.sqrt(3) * (hex.r + hex.q/2)
        };
    }

    function cartesianToHex(cart) {
        let q = p.round(cart.x * 2/3 / HEX_SIZE);
        let r = p.round((-cart.x/3 + p.sqrt(3)/3*cart.y) / HEX_SIZE);
        let s = -q-r;
        return {q: q, r: r, s: s};
    }

    class HexCell {
        constructor(q, r) {
            this.q = q;
            this.r = r;
            this.s = -q-r;

            this.isSelected = false;
        }

        get length() {
            return (
                p.abs(this.q) +
                p.abs(this.r) +
                p.abs(this.s)
            ) / 2;
        }

        get isHovered() {
            let cart = {
                x: p.mouseX - CANVAS_WIDTH/2,
                y: p.mouseY - CANVAS_HEIGHT/2
            };
            let that = cartesianToHex(cart);
            return this.distance(that) < 1;
        }

        get hex() {
            return {q: this.q, r: this.r, s: this.s};
        }

        distance(that) {
            return (
                p.abs(this.q - that.q) +
                p.abs(this.r - that.r) +
                p.abs(this.s - that.s)
            ) / 2;
        }

        setLabel() {
            let red   = (v) => "<span style='color: red;'>"   + v + "</span>";
            let green = (v) => "<span style='color: green;'>" + v + "</span>";
            let blue  = (v) => "<span style='color: blue;'>"  + v + "</span>";
            let paren = (v1, v2, v3) => "(" + v1 + ", " + v2 + ", " + v3 + ")";
            let eq = (v1, v2) => v1 + " = " + v2;
            label.innerHTML = eq(
                paren(red("q"), green("r"), blue("s")),
                paren(red(this.q), green(this.r), blue(this.s))
            );
        }

        setColor() {
            p.strokeWeight(6);
            if (this.isSelected) {
                p.fill(60, 80, 60);
                p.stroke(200, 220, 240);
            } else if (this.isHovered) {
                p.fill(240, 180, 80);
                p.stroke(100, 0, 0);
                this.setLabel();
            } else {
                p.fill(200, 220, 240);
                p.stroke(60, 80, 60);
            }
        }

        select() {
            this.isSelected ^= true;
        }

        draw() {
            this.setColor();
            let cart = hexToCartesian(this.hex);
            p.beginShape();
            for(let i=0; i<6; i++) {
                let x = cart.x + 0.80*HEX_SIZE*p.cos(p.radians(60*i));
                let y = cart.y + 0.80*HEX_SIZE*p.sin(p.radians(60*i));
                p.vertex(x, y);
            }
            p.endShape(p.CLOSE);
        }

        vertex() {
            let cart = hexToCartesian(this.hex);
            p.vertex(cart.x, cart.y);
        }
    }

    let __cellAssoc = {};
    function hasCell(hex) {
        return [hex.q, hex.r] in __cellAssoc;
    }
    function getCell(hex) {
        return __cellAssoc[[hex.q, hex.r]];
    }
    function setCell(hex, cell) {
        __cellAssoc[[hex.q, hex.r]] = cell;
    }
    function getAllCells() {
        return Object.values(__cellAssoc);
    }

    function drawCoordinate() {
        let num = CELL_NUM + 2;
        let drawOne = (col, deg, isPoints, weight) => {
            p.push();
            p.rotate(p.radians(deg));
            p.strokeWeight(weight);
            p.stroke(col);
            p.noFill();
            if (isPoints) {
                p.beginShape(p.POINTS);
                for(let i=-num+1; i<num; i++) {
                    p.vertex(HEX_SIZE * 3/2 * i, 0);
                }
            } else {
                p.beginShape();
                for(let i=-num; i<=num; i++) {
                    p.vertex(HEX_SIZE * 3/2 * i, 0);
                }
                p.vertex(HEX_SIZE * 3/2 * (num - 1), HEX_SIZE);
                p.vertex(HEX_SIZE * 3/2 * num, 0);
                p.vertex(HEX_SIZE * 3/2 * (num - 1), -HEX_SIZE);
                p.vertex(HEX_SIZE * 3/2 * num, 0);
            }
            p.endShape();
            p.pop();
        }
        drawOne(p.color(240, 90, 90, 120), 0, false, 5);
        drawOne(p.color(240, 70, 70, 160), 0, true, 8);
        drawOne(p.color(70, 200, 70, 120), 120, false, 5);
        drawOne(p.color(50, 200, 50, 160), 120, true, 8);
        drawOne(p.color(90, 90, 240, 120), 240, false, 5);
        drawOne(p.color(70, 70, 240, 160), 240, true, 8);
    }

    function shot() {
        let cart = {
            x: p.mouseX - CANVAS_WIDTH/2,
            y: p.mouseY - CANVAS_HEIGHT/2
        };
        let hex = cartesianToHex(cart);
        if (hasCell(hex)) {
            let cell = getCell(hex);
            cell.select();
            let goStraight = (diff) => {
                let i=0;
                let rec = () => {
                    i++;
                    let h = {
                        q: cell.q + i*diff.q,
                        r: cell.r + i*diff.r,
                        s: cell.s + i*diff.s
                    };
                    if (!hasCell(h)) return;
                    getCell(h).select();
                    setTimeout(rec, 100);
                };
                rec();
            };
            goStraight({q: +0, r: +1, s: -1});
            goStraight({q: +0, r: -1, s: +1});
            goStraight({q: -1, r: +0, s: +1});
            goStraight({q: +1, r: +0, s: -1});
            goStraight({q: +1, r: -1, s: +0});
            goStraight({q: -1, r: +1, s: +0});
        }
    }

    function refresh() {
        getAllCells().forEach(
            cell => cell.isSelected = false
        );
    }

    p.setup = () => {
        p.strokeCap(p.ROUND);
        p.strokeJoin(p.ROUND);
        p.createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
        let num = 100;
        let grid = iota(num).map(
            i => i - num/2
        ).map(
            q => iota(num).map(
                j => j - num/2
            ).map(
                r => new HexCell(q, r)
            ).filter(
                cell => cell.length <= CELL_NUM
            )
        );
        grid.forEach(
            cells => cells.forEach(
                cell => setCell(cell.hex, cell)
            )
        );
    };

    let shouldDrawCoordinate = true;

    p.draw = () => {
        p.background(255);
        p.translate(CANVAS_WIDTH/2, CANVAS_HEIGHT/2);
        getAllCells().filter(
            cell => cell.length <= CELL_NUM
        ).forEach(
            cell => cell.draw()
        );

        if (shouldDrawCoordinate) drawCoordinate();
    };

    p.mousePressed = () => {
        let cart = {
            x: p.mouseX - CANVAS_WIDTH/2,
            y: p.mouseY - CANVAS_HEIGHT/2
        };
        let hex = cartesianToHex(cart);
        if (hasCell(hex)) {
            getCell(hex).select();
        }
    }

    p.keyPressed = () => {
        if (p.keyCode == 32) { // space key
            shot();
        }
        if (p.keyCode == 90) { // z key
            shouldDrawCoordinate ^= true;
        }
        if (p.keyCode == 88) { // x key
            refresh();
        }
    }

};

const app = new p5(sketch, document.querySelector("#sketch"));

補足

Hexagonal Coordinatesを知ったきっかけ

昔、Shadertoyでこの作品を見つけました。

どうやって実装してるのか気になってコードを眺めるとどうやらcoordToHexhexToCellという関数で座標変換をしてるらしい。
おもしろそうと思ってぐぐってみたらHexagonal Coordinatesというものがあることを知りました。

スライドの最後にもあるように

こちらのサイトの説明がわかりやすいので、詳しく知りたい人は見てみてください。

Hexcells

Hexcellsはパズルゲームです。おもしろいのでぜひ。