Next.jsでゲーム作る (2)
Next.jsでゲーム作る記事の続きです.
前回の記事はこちらからどうぞ
データの取得
今回使うのは電車,バス,飛行機,船それぞれの停車,停留地点のみです.ありがたいことに国土交通省がこれらの座標や名前を無料で公開してくれています.ここからデータを使いやすいように加工していきます.ちなみに,現状半分ほどは非商用なので広告はつけられません.
バス停留所データ(非商用)
各都道府県ごとにシェイプファイルで取得できるのですべてダウンロードした後,解凍します.47個やるのはめんどくさいので自動化します.また,フォルダを1つに統合しシェイプファイルをgeojsonを経て都道府県別jsonに変換します.
unpack.py
import os
import sys
import shutil
def main():
try:
dir = input('input dir name: ')
files = os.listdir(dir)
for file in files:
shutil.unpack_archive(f'{dir}/{file}', f'{dir}/out')
sys.stdout.write(f'\033[2K\033[G {file} is unpacked')
sys.stdout.flush()
except Exception as e:
print(e)
exit(1)
merge_dirs.py
import os
import sys
import shutil
def main():
try:
src = input('input source dir name: ')
des = input('input destination dir name: ')
rmv = input('remove dirs automatically? (y): ')
os.makedirs(des, exist_ok=True)
dirs = os.listdir(src)
for dir in dirs:
files = os.listdir(f'{src}/{dir}')
for file in files:
shutil.move(f'{src}/{dir}/{file}', des)
sys.stdout.write(f'\033[2K\033[G {dir} is merged')
sys.stdout.flush()
if rmv == 'y':
os.rmdir(f'{src}/{dir}')
if rmv == 'y':
os.rmdir(src)
except Exception as e:
print(e)
exit(1)
shp2geo.py
import sys
import glob
import shapefile as shp
import json
def main():
try:
src = input('input source dir name: ')
des = input('input destination name: ')
files = glob.glob(f'{src}/*.shp')
for file in files:
sf = shp.Reader(file, encoding='cp932')
fields = sf.fields[1:]
names = [field[0] for field in fields]
buffer = []
for record in sf.shapeRecords():
atr = dict(zip(names, record.record))
geom = record.shape.__geo_interface__
buffer.append(dict(type='Feature', geometry=geom, properties=atr))
name = file.replace('.shp', '.geojson').replace(f'{src}\\', '')
with open(f'{des}/{name}', mode='w', encoding='UTF-8') as f:
json.dump({'type': 'FeatureCollection', 'features': buffer}, f, indent=2, ensure_ascii=False)
sys.stdout.write(f'\033[2K\033[G converted to {name}')
sys.stdout.flush()
except Exception as e:
print(e)
exit(1)
abstruct_data.py
import sys
import glob
import json
def main():
try:
src = input('input source dir name: ')
des = input('input destination dir name: ')
attr = input('input attribute name for bus stop name: ')
files = glob.glob(f'{src}/*.geojson')
for file in files:
with open(file, mode='r', encoding='UTF-8') as f:
ctx = json.load(f)
arr = [{'coordinate':s['geometry']['coordinates'],'name':s['properties'][attr]} for s in ctx['features']]
name = file.replace(src, des).replace('.geojson', '.json')
with open(name, mode='w', encoding='UTF-8') as f:
json.dump({'bus_stop': arr}, f, ensure_ascii=False, indent=2)
sys.stdout.write(f'\033[2K\033[G converted {file}')
sys.stdout.flush()
except Exception as e:
print(e)
exit(1)
空港データ(商用)
空港データはgeojsonがそのまま手に入りますが,座標と空港名が別ファイルなので個別に統合します.merge_airport_geo.py
import json
def main():
point = input('input Airport file name: ')
ref = input('input AirportReference name: ')
with open(point, mode='r', encoding='UTF-8') as f:
points = json.load(f)
with open(ref, mode='r', encoding='UTF-8') as f:
refs = json.load(f)
ref_map = {p['properties']['C28_000']:p['geometry']['coordinates'] for p in refs['features']}
arr = [
{'coordinate':ref_map[p['properties']['C28_101'].replace('#','')],'name':p['properties']['C28_005']}
for p in points['features']
]
arr = list(map(json.loads, set(map(json.dumps, arr))))
# https://qiita.com/kilo7998/items/184ed972571b2e202b40
name = point.replace('.geojson', '.json')
with open(name, mode='w', encoding='UTF-8') as f:
json.dump({'airport': arr}, f, ensure_ascii=False, indent=2)
鉄道データ(おそらく非商用)
こちらもgeojsonを整形していきます.abstruct_train_data.py
import json
def main():
try:
src = input('input file name: ')
with open(src, mode='r', encoding='UTF-8') as f:
ctx = json.load(f)
arr = [
{
'coordinate':s['geometry']['coordinates'][len(s['geometry']['coordinates'])//2],
'name':s['properties']['N02_005']
}
for s in ctx['features']
]
name = src.replace('.geojson', '.json')
with open(name, mode='w', encoding='UTF-8') as f:
json.dump({'bus_stop': arr}, f, ensure_ascii=False, indent=2)
except Exception as e:
print(e)
exit(1)
港湾データ(非商用)
geojsonに変換した後,必要な情報だけ抜き出します.abstruct_port_data.py
import json
def main():
try:
src = input('input file name: ')
with open(src, mode='r', encoding='UTF-8') as f:
ctx = json.load(f)
arr = [
{
'coordinate':s['geometry']['coordinates'],
'name':s['properties']['C02_005']
}
for s in ctx['features']
]
name = src.replace('.geojson', '.json')
with open(name, mode='w', encoding='UTF-8') as f:
json.dump({'bus_stop': arr}, f, ensure_ascii=False, indent=2)
except Exception as e:
print(e)
exit(1)
日本地図の出力
日本地図を出力する方法はいくつかありますが,今回は無料でかつ簡単にできるleafletを使います.
leafletのインストール
npm i leaflet react-leaflet
npm i -D @types/leaflet
Next.jsではSSRの関係でそのままでは使えないのでコンポーネント化したうえでダイナミックインポートをします.また,React leafletのバグ?でマーカーが正しく表示されないので設定を上書きします.
/components/map.tsx
import { MapContainer, TileLayer, useMap, Popup, Marker } from 'react-leaflet'
import L from 'leaflet'
import { useState } from 'react';
L.Icon.Default.imagePath = '';
L.Icon.Default.mergeOptions({
iconUrl: '/images/marker-icon.png',
iconRetinaUrl: '/images/marker-icon-2x.png',
shadowUrl: '/images/marker-shadow.png',
});
const types = ['blank', 'pale', 'MTB', 'dark', 'light'] as const
type MapType = typeof types[number]
const Map = () => {
const [type, setType] = useState<MapType>('blank');
return (
<div>
<select
defaultValue={type}
className='absolute top-3 left-16 z-[500] text-black border border-black rounded'
onChange={(e) => setType(e.target.value as MapType)}
>
{types.map(item => <option key={item}>{item}</option>)}
</select>
<MapContainer center={[35.68142790469971, 139.76706578914462]} zoom={13}>
{type === 'blank' && (
<TileLayer
attribution='<a href="http://maps.gsi.go.jp/development/ichiran.html" target="_blank">国土地理地理院</a>'
url='https://cyberjapandata.gsi.go.jp/xyz/blank/{z}/{x}/{y}.png'
/>
)}
{type === 'pale' && (
<TileLayer
attribution='<a href="http://maps.gsi.go.jp/development/ichiran.html" target="_blank">国土地理地理院</a>'
url='https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png'
/>
)}
{type === 'MTB' && (
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors & USGS'
url="http://tile.mtbmap.cz/mtbmap_tiles/{z}/{x}/{y}.png"
/>
)}
{type === 'dark' && (
<TileLayer
attribution='<a href="http://jawg.io" title="Tiles Courtesy of Jawg Maps" target="_blank">© <b>Jawg</b>Maps</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url={`https://{s}.tile.jawg.io/jawg-dark/{z}/{x}/{y}{r}.png?access-token=${process.env.NEXT_PUBLIC_JAWG_ACCESS_TOKEN}`}
/>
)}
{type === 'light' && (
<TileLayer
attribution='<a href="http://jawg.io" title="Tiles Courtesy of Jawg Maps" target="_blank">© <b>Jawg</b>Maps</a> © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url={`https://{s}.tile.jawg.io/jawg-light/{z}/{x}/{y}{r}.png?access-token=${process.env.NEXT_PUBLIC_JAWG_ACCESS_TOKEN}`}
/>
)}
<Marker position={[35.68142790469971, 139.76706578914462]}>
<Popup>
A pretty CSS3 popup. <br /> Easily customizable.
</Popup>
</Marker>
</MapContainer>
</div>
)
}
export default Map;
/pages/play.tsx
+ import dynamic from 'next/dynamic';
import useScene from '../libs/redux/hooks/useScene'
+ const Map = dynamic(() => import('../components/map'), {ssr: false})
const PlayScene = () => {
const { scene, handler: setScene } = useScene();
return (
<>
- <div>{scene} page</div>
- <button className='mr-4' onClick={() => setScene('start')}>back to start</button>
- <button onClick={() => setScene('result')}>go to result</button>
+ <button
+ className='z-[500] absolute right-4 top-4 border border-black text-black bg-white rounded'
+ onClick={() => setScene('start')}
+ >
+ back to start
+ </button>
+ <button
+ className='z-[500] absolute right-4 top-12 border border-black text-black bg-white rounded'
+ onClick={() => setScene('result')}
+ >
+ go to result
+ </button>
+ <Map />
</>
)
}
export default PlayScene
leafletはあくまでラスタ画像を表示する支援ツールなので,地図情報自体は持ちません.そこで外部のデータを読み込むことで地図を表示します.このサイトで良さそうな地図を探しました.
今回は国土地理院の2種とその他3種を選べる形式にすることにしました.
jawgについてはaccessTokenが必要なので,.env.local
にtokenを書いてNEXT_PUBLIC_
のプレフィックスをつけて呼び出します.また,無料枠が月50000viewまでなのでユーザーが多いと消えるかもしれません.
CanvasにMarkerを描画する
今後ゲーム内で表示するMarkerが何千個単位で増えてくると明らかに重くなるので,DOMによるレンダリングではなくhtmlのCanvasによるレンダリングに切り替えます.
これを使えば10万個のマーカーを1秒以内にレンダリングできます.
npm install leaflet-canvas-marker
/components/map.tsx
...
+ import 'leaflet-canvas-marker'
...
const Map = () => {
...
return (
<div>
<MapContainer ...>
...
+ <AirportLayer />
</MapContainer>
</div>
)
}
+ const AirportLayer = () => {
+ const map = useMap()
+ const CenterIcon = new L.Icon({
+ iconUrl: '/images/airport.svg',
+ iconSize: [24, 24],
+ iconAnchor: [12, 24],
+ popupAnchor: [0, -24],
+ tooltipAnchor: [0, -24],
+ shadowSize: [24, 24],
+ })
+ useEffect(() => {
+ const layer = L.canvasIconLayer().addTo(map)
+ const markers = airport.airport.map(p =>
+ L.marker(p.coordinate.reverse() as LatLngTuple, { icon: CenterIcon }).bindPopup(p.name)
+ )
+ layer.addMarkers(markers)
+ })
+ return null
+ }
型定義はされていないので,自前で用意する必要があります.知識が乏しいので今回は動けばいいや精神です.
/types/leaflet.d.ts
import 'leaflet'
import * as geojson from 'geojson'
declare module 'leaflet' {
export interface BBox {
minX: number
minY: number
maxX: number
maxY: number
data: Marker<geojson.GeoJsonProperties>
}
export class CanvasIconLayer extends Layer {
initialize(options: any): any
setOptions(options: any): Function
redraw(): void
addMarkers(markers: Marker<geojson.GeoJsonProperties>[]): void
addMarker(marker: Marker<geojson.GeoJsonProperties>): void
addLayer(layer: Layer): void
addLayers(layers: Layer[]): void
removeLayer(layer: Layer): void
removeMarker(marker: Marker<geojson.GeoJsonProperties>, redraw: boolean): void
onAdd(map: Map): void
onRemove(map: Map): void
addTo(map: Map): this
clearLayers(): void
_addMarker(marker: Marker<geojson.GeoJsonProperties>, latlng: LatLngExpression, isDisplaying: boolean): BBox[]
_drawMarker(marker: Marker<geojson.GeoJsonProperties>, pointPos: Point): void
_drawImage(marker: Marker<geojson.GeoJsonProperties>, pointPos: Point): void
_reset(): void
_redraw(clear: boolean): void
_initCanvas(): void
addOnClickListener(listener: any): void
addOnHoverListener(listener: any): void
_executeListeners(event: any): void
}
export function canvasIconLayer(): CanvasIconLayer
}
空港と同じようにバスや港,電車のLayerも追加します.地図とマーカーの位置が微妙にずれてますが,どうしようもないので今回は目をつむってください.
また,マーカーは以下のサイトを利用させていただきました.
次回はゲームシステムを作っていきます.
続きはこちら
参考文献
Author And Source
この問題について(Next.jsでゲーム作る (2)), 我々は、より多くの情報をここで見つけました https://zenn.dev/kage1020/articles/cd7f2346c7c357著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Collection and Share based on the CC protocol