تم تصميم منشئ الخرائط لمطوري الألعاب لتبسيط عملية التطوير وتحسين الكفاءة
العرض على الإنترنت: 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 )
}
}
}
}
الطريقة الأكثر فعالية لتخزين Tileset هي الأطلس أو ورقة الصور المتحركة. هذا هو كل المربعات المطلوبة مجمعة معًا في ملف صورة واحد. عندما يلزم رسم المربعات، يتم عرض جزء صغير فقط من هذه الصورة الأكبر على لوحة اللعبة. صانع RPG التالي هو جهاز 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 */
}
تحتاج إلى استخدام الماوس للانتقال إلى بلاطة معينة للحصول على رقم البلاطة المقابلة
ملاحظة: هناك ثلاث معلومات إحداثية يمكن الحصول عليها عن طريق أحداث الماوس، ويجب استخدام الإزاحة هنا للصفحة والإزاحة والعميل، وإلا سيحدث خطأ
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 } ,
]
يمكن استخدام هذا لتحقيق استبدال اللون
لا تحتوي خوارزمية ملء الحقن على مفهوم الحدود، ولكنها تستبدل فقط اللون المحدد في منطقة Unicom.
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 )
}
}
}