지도 생성기는 게임 개발자가 개발을 단순화하고 효율성을 향상할 수 있도록 설계되었습니다.
온라인 디스플레이: https://alsritter.gitee.io/map-editor-online
이 도구는 모바일 단말기에 적합하지 않습니다
현재 사용되는 브릭을 선택하세요
기본적으로(자신만의 방식으로 데이터를 구문 분석할 수 있습니다):
이 도구의 용도는 무엇입니까?
탄생 지점과 종료 지점 설정
다음은 몇 가지 작동 원리입니다. 이 부분에 관심 없으신 분들은 바로 건너뛰셔도 됩니다~
< style >
canvas {
border: 1px solid black;
}
</ style >
< canvas width =" 500 " height =" 500 " id =" testCanvas " > </ canvas >
< script type =" text/javascript " >
function draw ( ) {
const canvas = document . getElementById ( 'testCanvas' )
const ctx = canvas . getContext ( '2d' ) //取得2d 画布上下文
const _cols = 16
const _rows = 16
// 先获取每个图形格子的大小
const _space = canvas . width / _cols
// 绘制线条
for ( let i = 0 ; i < _cols ; i ++ ) {
ctx . beginPath ( ) ; // 开启路径,设置不同的样式
ctx . moveTo ( _space * i - 0.5 , 0 ) ; // -0.5是为了解决像素模糊问题
ctx . lineTo ( _space * i - 0.5 , canvas . height ) ;
ctx . setLineDash ( [ 1 , 2 ] ) ; //绘制虚线
ctx . strokeStyle = "#2a2a2a" ; // 设置每个线条的颜色
ctx . stroke ( ) ;
}
// 同理y轴
for ( let i = 0 ; i < _rows ; i ++ ) {
ctx . beginPath ( ) ; // 开启路径,设置不同的样式
ctx . moveTo ( 0 , _space * i - 0.5 ) ;
ctx . lineTo ( canvas . width , _space * i - 0.5 ) ;
ctx . strokeStyle = "#2a2a2a" ;
ctx . stroke ( ) ;
}
}
window . addEventListener ( 'load' , draw , false )
</ script >
참고: 마우스 이벤트로 얻을 수 있는 좌표 정보는 세 가지가 있습니다. 여기서는 페이지, 오프셋, 클라이언트에 대해 오프셋을 사용해야 합니다. 그렇지 않으면 오류가 발생합니다.
console . log ( 'page: ' , e . pageX , e . pageY )
console . log ( 'offset: ' , e . offsetX , e . offsetY )
console . log ( 'client: ' , e . clientX , e . clientY )
const map = [ ]
// 先初始化 map
for ( let y = 0 ; y < _rows ; y ++ ) {
const temp = [ ]
for ( let x = 0 ; x < _cols ; x ++ ) {
temp . push ( 0 )
}
map . push ( temp )
}
// 监听鼠标事件,判断当前点击了哪个格子
canvas . onmousedown = ( e ) => {
const x = Math . floor ( e . offsetY / _space )
const y = Math . floor ( e . offsetX / _space )
// 点击更新该区域的编号
map [ y ] [ x ] = 1
// 刷新画布
for ( let y = 0 ; y < _rows ; y ++ ) {
for ( let x = 0 ; x < _cols ; x ++ ) {
if ( map [ x ] [ y ] !== 0 ) {
// 绘制
ctx . fillRect ( x * _space , y * _space , _space , _space )
}
}
}
}
타일셋을 저장하는 가장 효과적인 방법은 아틀라스나 스프라이트 시트입니다. 이는 단일 이미지 파일에 함께 그룹화된 모든 필수 타일입니다. 타일을 그려야 할 때 이 큰 이미지의 작은 부분만 게임 캔버스에 표시됩니다. 다음 RPGMaker는 사용되는 Tileser입니다.
튜토리얼에서 사용된 타일셋은 다음과 같습니다.
첫 번째 단계는 이 지도책을 자르는 것입니다(여기에는 데이터를 얻는 방법에 대한 직접적인 데모가 있으며 다음 섹션에서 실제로 사용하기 시작합니다).
function draw ( ) {
let ctx = document . getElementById ( 'canvas' ) . getContext ( '2d' )
let img = new Image ( )
img . onload = ( ) => {
let tileColsNum = 5 ; // 图的宽度,以列表示
let tileRowsNum = 1 ; // 图的高度,以行为中
let sWidth = img . width / tileColsNum ; // 切图的宽度
let sHeight = img . height / tileRowsNum ; // 切图的高度
for ( let col = 0 ; col < tileColsNum ; col ++ ) {
for ( let row = 0 ; row < tileRowsNum ; row ++ ) {
ctx . drawImage ( img ,
col * sWidth , // 开始切的 X 位置
row * sHeight , // 开始切的 Y 位置
sWidth , sHeight , // 切的高度和宽度
col * ( sWidth + 10 ) , row * sHeight , // 显示的位置
sWidth , sHeight ) // 显示的高度和宽度
}
}
}
img . src = './src/img/tiles.png'
}
window . addEventListener ( 'load' , draw , false )
그리기 효과는 다음과 같습니다
// 根据鼠标点击取得格子
canvas . onmousedown = ( e ) => {
console . log ( map [ Math . floor ( e . offsetY / _space ) ] [ Math . floor ( e . offsetX / _space ) ] )
}
/**
* 绘制背景方格
* @param {CanvasRenderingContext2D} ctx 传入 canvas 的 Context
* @param {Number} width 画布的宽度
* @param {Number} height 画布的高度
*/
static drawBackground ( ctx , width , height ) {
let emptyBox = ctx . createImageData ( width , height )
let emptyBoxData = emptyBox . data
// 通过 canvas宽高 来遍历一下 canvas 上的所有像素点
for ( let i = 0 ; i < height ; i ++ ) {
for ( let j = 0 ; j < width ; j ++ ) {
let point = ( i * width + j ) << 2 // << 相当于 * 4
let rgbData = ( ( i >> 2 ) + ( j >> 2 ) ) & 1 ? 204 : 255 // >> 2 相当于 / 4 取整, & 1相当于 % 2
emptyBoxData [ point ] = rgbData
emptyBoxData [ point + 1 ] = rgbData
emptyBoxData [ point + 2 ] = rgbData
emptyBoxData [ point + 3 ] = 255
}
}
ctx . putImageData ( emptyBox , 0 , 0 )
}
그래프 데이터를 저장하려면 사용자 정의 클래스를 사용할 수 있습니다.
/**
* 单个 Tile 在图片的位置
*/
class Tile {
/**
* Tile 在贴图里面的位置,以及保存它的路径偏移量(贴图位置和路径偏移量无关,后者是保存它显示在屏幕的位置)
* @param {Number} x Tile 在贴图里的起始 x
* @param {Number} y Tile 在贴图里的起始 y
*/
constructor ( x , y ) {
this . x = x
this . y = y
}
}
/**
* TileImage 里面的 Tile
*/
export class TileMap {
/**
*
* @param {Number} cols Tile贴图的宽度(一列有多少个 Tile)
* @param {Number} rows Tile贴图的高度(一行有多少个 Tile)
* @param {HTMLImageElement} img 这里传入的 Tile 贴图,必须放在 onload 里面执行
*/
constructor ( cols , rows , img ) {
this . cols = cols
this . rows = rows
this . img = img
this . tiles = [ ]
this . sWidth = 0 // 每个单元格的宽度
this . sHeight = 0 // 每个单元格的高度
this . sWidth = this . img . width / this . cols // 切图的宽度
this . sHeight = this . img . height / this . rows // 切图的高度
for ( let col = 0 ; col < this . cols ; col ++ ) {
for ( let row = 0 ; row < this . rows ; row ++ ) {
this . tiles . push ( new Tile ( col * this . sWidth , row * this . sHeight ) )
}
}
}
/* 省略一堆 getter */
}
해당 Tile 번호를 얻기 위해 특정 Tile로 이동하는 마우스 구현 필요
참고: 마우스 이벤트로 얻을 수 있는 좌표 정보는 세 가지가 있습니다. 여기서는 페이지, 오프셋, 클라이언트에 대해 오프셋을 사용해야 합니다. 그렇지 않으면 오류가 발생합니다.
console . log ( 'page: ' , e . pageX , e . pageY )
console . log ( 'offset: ' , e . offsetX , e . offsetY )
console . log ( 'client: ' , e . clientX , e . clientY )
onmouseup, onmouseout, onmousedown 세 가지 이벤트가 주로 사용됩니다.
// 监听鼠标事件,松手时刷新画布
canvas . onmouseup = ( e ) => {
ctx . clearRect ( 0 , 0 , canvas . width , canvas . height )
DrawUtility . drawAllTile ( ctx , map , posList )
}
// 移出画布也刷新
canvas . onmouseout = ( e ) => {
ctx . clearRect ( 0 , 0 , canvas . width , canvas . height )
DrawUtility . drawAllTile ( ctx , map , posList )
}
// 监听鼠标事件,判断当前点击了哪个区域
canvas . onmousedown = ( e ) => {
for ( let index = 0 ; index < map . getTiles ( ) . length ; index ++ ) {
if (
ctx . isPointInPath (
posList . getTilePosOfArray ( index ) . path ,
e . offsetX ,
e . offsetY
)
) {
console . log ( `点击了 ${ index } ` )
DrawUtility . drawDarkTile ( ctx , posList , index )
}
}
}
레이어 변경, 새 데이터 가져오기, 디스플레이 모드 변경 등 많은 상태 변경이 포함되므로 이벤트를 사용하여 이를 분리해야 하며 제어 레이어는 특정 시간의 발생만 모니터링하면 됩니다. .
예를 들어 특정 상태가 변경되면 새로 고침 이벤트가 발생해야 합니다.
// 监听显示模式(这里对 Vuex 的值进行监听)
$store . watch (
( ) => $store . state . isShowAllLayer ,
val => {
isShowAll = new Boolean ( val ) . valueOf ( ) ;
window . dispatchEvent ( new CustomEvent ( "refreshData" ) ) ; // 通知更新数据
}
) ;
그리고 컨트롤 레이어에서 이 이벤트를 모니터링하고 새로 고칩니다.
// 定义一个刷新事件的监听
window . addEventListener ( "refreshData" , ( ) => {
// 这里进行刷新操作
} ) ;
이 인출은 실제로 쌓고 터지는 과정이므로 스스로 스택을 유지할 수 있습니다.
import Grid from "./VO/Grid" ;
/**
* 自定义的栈结构,主要用来维护 画布数据
*/
export default class MapStack {
private arr : Array < { layer : number ; map : Grid [ ] [ ] } > ;
constructor ( ) {
this . arr = [ ] ;
}
/**
* 压栈操作
* @param { { layer: number, map: Grid[][] }} mapInfo
*/
push ( mapInfo : { layer : number ; map : Grid [ ] [ ] } ) : void {
this . arr . push ( mapInfo ) ;
}
/**
* 退栈操作
*/
pop ( ) : { layer : number ; map : Grid [ ] [ ] } {
return this . arr . pop ( ) as { layer : number ; map : Grid [ ] [ ] } ;
}
/**
* 获取栈顶元素
*/
top ( ) : { layer : number ; map : Grid [ ] [ ] } {
return this . arr [ this . arr . length - 1 ] ;
}
/**
* 清空栈
*/
clear ( ) : boolean {
this . arr = [ ] ;
return true ;
}
}
각 쓰기 후에 적시에 스택에 푸시
그런 다음 제어 레이어에서 모니터링 Ctrl + Z
// 监听撤回键(使用栈)
document . onkeydown = e => {
if ( e . ctrlKey == true && e . key == "z" ) {
// 如果栈内不为空才撤回
if ( recallMap . size ( ) !== 0 ) {
// 弹栈
const temp = recallMap . pop ( ) ;
gridManagerArray [ temp . layer ] . setMap ( temp . map ) ;
window . dispatchEvent ( refreshEvent ) ; // 通知更新数据
}
}
} ;
프레임마다 모든 데이터를 새로 고치면 많은 성능이 낭비되고 데이터가 여러 레이어로 구성되어 성능이 저하됩니다.
수정된 위치를 기록하기 위해 캐시맵을 생성합니다. 다음 프레임에 업데이트됩니다.
import BasePos from "./VO/BasePos" ;
export default class CacheMap {
private cols : number ;
private rows : number ;
private map : boolean [ ] [ ] ;
constructor ( cols : number , rows : number ) {
this . cols = cols ;
this . rows = rows ;
this . map = [ ] ;
// 每个数组都需要先初始化 默认是 false
for ( let i = 0 ; i < rows ; i ++ ) {
const temp : boolean [ ] = [ ] ;
for ( let j = 0 ; j < cols ; j ++ ) {
temp . push ( false ) ;
}
this . map . push ( temp ) ;
}
}
/**
* 返回被修改的位置
* @returns {ModifiedPos[]} 里面是被修改的位置,需要被更新
*/
getChange ( ) : BasePos [ ] {
const list : BasePos [ ] = [ ] ;
// 如果有被修改的则把这个位置添加到 List 里面
for ( let i = 0 ; i < this . rows ; i ++ ) {
for ( let j = 0 ; j < this . cols ; j ++ ) {
if ( this . map [ i ] [ j ] ) {
list . push ( new BasePos ( i , j ) ) ;
}
}
}
return list ;
}
/**
* 标识这个地方被修改了
* @param x 被修改的 x 坐标
* @param y 被修改的 y 坐标
*/
setChange ( x : number , y : number ) : void {
this . map [ x ] [ y ] = true ;
}
/* ............. */
/**
* 当更新完成之后要归零
*/
cleanChange ( ) : void {
for ( let i = 0 ; i < this . rows ; i ++ ) {
for ( let j = 0 ; j < this . cols ; j ++ ) {
this . map [ i ] [ j ] = false ;
}
}
}
}
그리고 매 프레임마다 데이터를 새로 고친 후 캐시가 변경된 후에만 다시 렌더링됩니다.
// 局部刷新
const modif = cacheMap . getChange ( ) ;
for ( let i = 0 ; i < modif . length ; i ++ ) {
// 先清空指定的位置
DrawTools . clearTile (
ctx ,
space ,
gridManagerArray [ layer ] . getGrid ( modif [ i ] . x , modif [ i ] . y ) . x ,
gridManagerArray [ layer ] . getGrid ( modif [ i ] . x , modif [ i ] . y ) . y
) ;
// 因为有多层数据,所以这里遍历刷新每一层的这个位置
for ( let j = 0 ; j < gridManagerArray . length ; j ++ ) {
RendererTools . changeTile (
gridManagerArray ,
j ,
modif ,
i ,
tileManager ,
ctx ,
space
) ;
}
// 更新完成后要归零
cacheMap . cleanChange ( ) ;
실제로 PS의 페인트통은 일반적으로 채색에 사용됩니다. 예를 들어, 그려진 원을 클릭하면 밖으로 나가지 않고도 자동으로 채워집니다.
여기서는 4-unicom 알고리즘이 사용됩니다.
참고: 8-유니콤과 4-유니콤은 판정 횟수만 다릅니다. 파란색은 4유니콤, 빨간색은 8유니콤의 효과입니다.
// 4联通要判断的方向
const direction_4 = [
{ offsetX : - 1 , offsetY : 0 } ,
{ offsetX : 1 , offsetY : 0 } ,
{ offsetX : 0 , offsetY : - 1 } ,
{ offsetX : 0 , offsetY : 1 } ,
]
// 8联通要判断的方向
const direction_8 = [
{ offsetX : 0 , offsetY : - 1 } ,
{ offsetX : 0 , offsetY : 1 } ,
{ offsetX : - 1 , offsetY : 0 } ,
{ offsetX : 1 , offsetY : 0 } ,
{ offsetX : - 1 , offsetY : 1 } ,
{ offsetX : - 1 , offsetY : - 1 } ,
{ offsetX : 1 , offsetY : 1 } ,
{ offsetX : 1 , offsetY : - 1 } ,
]
이는 색상 교체를 달성하는 데 사용할 수 있습니다.
주입 충진 알고리즘은 경계의 개념이 없고 유니콤 영역에 지정된 색상 만 대체합니다.
function floodSeedFill ( map , x , y , oldValue , newValue , maxX , minX , maxY , minY ) {
// 要做边界值判断
if (
x > maxX ||
x < minX ||
y > maxY ||
y < minY ) {
return
}
// 递归条件就是某个方向上指定的位置为旧值
if ( map [ x ] [ y ] == oldValue ) {
map [ x ] [ y ] = newValue
for ( let i = 0 ; i < direction_4 . length ; i ++ ) {
const newX = x + direction_4 [ i ] . offsetX
const newY = y + direction_4 [ i ] . offsetY
floodSeedFill ( map , newX , newY , oldValue , newValue , maxX , minX , maxY , minY )
}
}
}
이는 위의 주입 및 충전 방향과 다릅니다. 이는 경계(지정된 색상이 경계임)에 대한 자세한 내용으로, 지정된 경계 내의 모든 값을 대체합니다.
function BoundarySeedFill ( map , x , y , boundaryValue , newValue , maxX , minX , maxY , minY ) {
// 要做边界值判断
if (
x > maxX ||
x < minX ||
y > maxY ||
y < minY ) {
return
}
if ( map [ x ] [ y ] !== boundaryValue && map [ x ] [ y ] !== newValue ) {
map [ x ] [ y ] = newValue
for ( let i = 0 ; i < direction_4 . length ; i ++ ) {
const newX = x + direction_4 [ i ] . offsetX
const newY = y + direction_4 [ i ] . offsetY
BoundarySeedFill ( map , newX , newY , boundaryValue , newValue , maxX , minX , maxY , minY )
}
}
}