pages/play-puzzle/
|
└──index.tsx
copmonents/play-puzzle
|
└──index.tsx
└──/puzzle-canvas
└──index.tsx
└──create-puzzle.tsx
└──move-puzzle.tsx
└──audio-effect.tsx
└──complete-animation.tsx
└──config-init.tsx
└──find-change.tsx
PuzzleCanvas
컴포넌트를 화면에 출력한다.setConfig
, createTiles
socket.emit("groupIndex")
, socket.emit("getPuzzleConfig")
, Puzzle.setting()
Puzzle.move()
initConfig
이 방의 첫 번째 유저라면
import { createTiles } from "@components/play-puzzle/puzzle-canvas/puzzle/create-puzzle";
if (isFirstClient) {
setConfig(puzzleImg, level, Paper);
createTiles();
config = Puzzle.exportConfig();
Puzzle.move(isFirstClient, socket, roomID);
socket.emit("setPuzzleConfig", { roomID: roomID, config: config });
}
이 방의 첫 번째 유저가 아니라면
else {
socket.emit("groupIndex", { roomID: roomID, groupTileIndex: 200 });
socket.on("groupIndex", ({ groupIndex }: { groupIndex: number }) => {
if (!isNaN(groupIndex)) {
Puzzle.groupFirstUpdate(groupIndex);
}
});
socket.on("getPuzzleConfig", (res: Config) => {
Puzzle.setting(getConfig(res, Paper));
Puzzle.move(isFirstClient, socket, roomID);
});
socket.emit("getPuzzleConfig", { roomID: roomID });
}
Puzzle Config
return {
originHeight : img.current.height; //실제 사진의 높이
originWidth: img.current.width; //실제 사진의 너비
imgWidth: originHeight >= originWidth
? Math.round((levelSize[level] * originWidth) / originHeight / 100) * 100
: levelSize[level]; // canas에 나타날 이미지의 너비
imgHeight: originHeight >= originWidth
? levelSize[level]
: Math.round((levelSize[level] * originHeight) / originWidth / 100) * 100; // canvas에 아나탈 이미지의 높이
tilesPerRow: Math.floor(imgWidth / tileWidth); // 한 줄에 타일 개수
tilesPerColumn: Math.floor(imgHeight / tileWidth); // 한 열에 타일 개수
tileWidth: 100; // 한 타일의 길이(타일은 정사각형)
tileMarginWidth: number; // 타일이 잘 맞기 위한 사이값
level: number; // 난이도
imgName: String; // img 태그의 id값
groupTiles: any[]; // 그룹화된 타일들 배열 ([타일, 해당 타일의 그룹(없을 땐 undefined)])
shapes: any[]; // 랜덤으로 생성된 각 타일들의 shape 정보
tiles: any[]; // 타일들 배열
complete: boolean; // 퍼즐 성공 여부
groupTileIndex: number | null; // 그룹화된 타일들 배열의 인덱스
project: any; // html의 캔버스를 감싼 paper 변수. paper 객체를 조작할 떄 필요
puzzleImage: any; // paper.Raster 객체
tileIndexes: any[]; // 타일들 배열의 인덱스
};
getMask
에서 위에서 랜덤으로 생성한 모양값 따라 퍼즐 곡선을 만든 paper.path()를 반환받는다.for (let y = 0; y < config.tilesPerColumn; y++) {
for (let x = 0; x < config.tilesPerRow; x++) {
const shape = config.shapes[y * config.tilesPerRow + x];
const mask = getMask(...);
//mask.opacity, mask.strokeColor 퍼즐 투명도와 퍼즐색 설정
//퍼즐의 모양을 복사하여 테두리용 path 생성
const img = getTileRaster(
config.puzzleImage.clone(),
new Size(config.tileWidth, config.tileWidth),
new Point(config.tileWidth * x, config.tileWidth * y),
Math.max(
config.imgWidth / config.originWidth,
config.imgHeight / config.originHeight
)
);
const border = mask.clone();
border.strokeColor = new config.project.Color("#ddd");
// 퍼즐 모양, 퍼즐 배경 이미지, 퍼즐 테두리를 group으로 묶어주면 하나의 타일이 됨
const tile = new config.project.Group([mask, img, border]);
config.tile.push(tile);
config.groupTiles.push([tile, undefined]);
config.groupArr.push(undefined);
config.tileIndexes.push(config.tileIndexes.length);
}
}
Puzzle.setting({...config,});
을 호출해 puzzleConfig를 업데이트한다.//_parent와 index는 paperjs의 path의 Project Hierarchy이다.
select_idx = gtile[0].index //해당타일 index;
gtile[0]._parent.addChild(gtile[0]); //해당타일
if (gtile[1] === undefined) {
//그룹화안된 타일을 움직이고 있다면 바뀐 위치를 바로 넣어준다.
gtile[0].position = new Point(newPosition.x, newPosition.y);
} else {
//그룹화된 타일을 움직이고 있다면 같은 그룹인 타일의 위치도 바꿔준다.
config.groupTiles.forEach((gtile_now) => {
if (gtile[1] === gtile_now[1]) {
gtile_now[0].position = new Point(
gtile_now[0].position._x + newPosition.x - originalPosition.x,
gtile_now[0].position._y + newPosition.y - originalPosition.y
);
}
});
}
마우스 클릭을 손에서 떼었을 때, 해당 타일의 그룹화 여부 확인 후 그룹화를 하고 위치보정을 한다. 변화된 위치를 socket.emit("tilePosition")으로 서버의 퍼즐 정보에 저장한다. 선점을 해제한다.
fitTiles()
함수를 호출한다.fitTiles
는 mouseUp한 타일과 근처 타일(상하좌우 타일 중 1)이 합쳐져야 하는지 판단하고, preTile과 nowTile의 위치를 보정해준다.(근처에 대충 맞춰도 연결된 타일들로 딱 맞춰준다) 때문에 위치보정만을 위해서도 호출된다.(groupFit
) 위치보정을 위한 호출이 아닐 때, 두 타일이 합쳐져야 한다면 uniteFlag의 값을 true로 바꾼다. (위치보정을 위해서 FindChange 컴포넌트의 함수들을 사용한다.)uniteTiles(nowTile, preTile)
함수를 호출한다.uniteTiles(nowTile, preTile)
는 nowTile의 그룹의 유무, preTiles의 그룹의 유무를 고려하며 두 타일을 합친다. 두 타일 모두 그룹이 있다면 preTile의 그룹이 우선순위가 높다.(nowTile의 타일 또는 그룹의 타입들은 preTile의 그룹에 들어간다.)타일의그룹
에 preTile이나 nowTile의 그룹인덱스를 넣으면 그룹화가 되었다고 간주한다.uniteTiles
에서 nowTile과 preTile 모두가 그룹화되어 있지 않다면 (※groupIndex 질문하기)uniteTiles
에서 dismantling을 하고 그룹과 그룹간의 퍼즐을 맞게 하기 위해 groupFit
을 호출한다. (※dismantling 리팩토링할까?) (dismantling은 groupTiles에서 nowTile의 타일의그룹
정보를 undefined하는 작업을 말함)groupFit
은 퍼즐 그룹과 퍼즐 그룹을 합칠 때 위치 보정을 위한 함수이다.groupFit
은 이 문제를 해결하는 함수이다.fitTiles
를 호출한다.config.groupTiles.forEach((gtile, idx) => {
if (gtile[0] === tile) {
//이번에 이동한 타일이 그룹화가 되지 않았을 때
if (gtile[1] === undefined) {
socket.emit("tilePosition", {
roomID: roomID,
tileIndex: idx,
tilePosition: gtile[0].position,
tileGroup: gtile[1],
changedData: gtile[0],
});
} else {
//이번 이동한 타일이 그룹화가 되었다면
//groupTiles의 모든 원소들의 0번째엔 해당 타일이 없다.
config.groupTiles.forEach((tileNow, index) => {
if (gtile[1] === tileNow[1]) {
//이번 이동한 타일의 그룹과 같은 그룹에 있는 타일들
socket.emit("tilePosition", {
roomID: roomID,
tileIndex: index,
tilePosition: tileNow[0].position,
tileGroup: tileNow[1],
changedData: tileNow[0],
});
}
});
}
}
});
fitTiles
는 mouseUp한 타일과 근처 타일(상하좌우 타일 중 1)이 합쳐져야 하는지 판단하고, preTile과 nowTile의 위치를 보정해준다.(근처에 대충 맞춰도 연결된 타일들로 딱 맞춰준다) 때문에 두 타일을 합칠 필요가 없어도 위치보정만을 위해서도 groupFit
에서 호출된다.
위치 보정이란 무엇인가
위의 경우 상-하 퍼즐을 맞출 때, y축 이동과 x축 이동을 고려해야 퍼즐이 육안으로 보기에 정확히 맞춰진다. 위의 퍼즐을 기준으로 보았을 때 아래 퍼즐의 y가 오른쪽으로 이동해야 한다. 상하 퍼즐끼리 맞출때 y축, x축 이동을 구현한 함수를 yUp, xUp이라 했다.
반대로 좌-우 퍼즐끼리 맞출 때 y축, x축 이동을 구현한 함수를 yChnage, xChange라고 했다.
groupTiles의 모든 타일들의 그룹이 undefined이며 같을 때 퍼즐이 완성되었다 간주한다.