해당 페이지에 들어오면 본 게임 시작 전에 미니 게임이 진행된다. 키보드를 이용해 지도 위에 있는 전기차를 이동시켜 주유소에 도달하면 본 게임으로 들어가게 된다.
본 게임은 스페이스바를 눌러 다가오는 장애물을 점프로 피해 목적지에 도달하는 게임이다. 목적지인 주유소는 Score가 150점에 도달하면 화면상에 나타나게 해 놓았고 주유소에 도달하면 게임 성공에 관련 된 모달이 뜬다. 성공 모달에서 찌리릿 코인을 받을 수 있으며 이 코인을 10개 모으면 쿠폰으로 교환할 수 있도록 다른 카테고리에 구현해 놓았다.
게임은 여러번 할 수 있지만 코인은 하루에 1개만 수집할 수 있도록 설정해 두었으며 게임을 진행하다 목적지에 도달하기 전에 장애물에 부딪히면 게임 실패에 관련 된 모달이 뜨고 실패 모달에서 다시하기 버튼을 눌러 게임을 다시 실행할 수 있다.
본 게임 화면 우측 상단에 회전하는 집을 클릭하면 "/home" 경로로 이동하도록 이벤트 리스너를 설정하였다 .
let canvas과 let ctx 부분은 게임 화면을 렌더링하는데 사용할 Canvas 요소와 2D Context를 가져온다.
electricCar 객체는 전기 자동차의 속성과 동작을 정의하고 있다. x, y는 자동차의 위치, width와 height는 자동차 이미지의 크기를 나타낸다. draw, jump, fall 메서드를 통해 자동차를 그리고, 점프와 낙하 동작을 처리하였다.
Box 클래스는 장애물을 나타낸다. draw 메서드를 통해 장애물을 생성하는데, 장애물의 위치는 화면 오른쪽 끝에서 시작하며 이미지는 장애물 이미지는 2개를 넣어 랜덤으로 번갈아 나오게 설정하였다.
<script>
const homeButton = document.querySelector(".home");
homeButton.addEventListener("click", function () {
window.location.href = "/home";
});
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
let currentCar = 0;
canvas.width = window.innerWidth;
var carImages = new Image();
carImages.src = "/cargame.png";
//전기차
let electricCar = {
x: 20,
y: 50,
width: 80,
height: 80,
draw() {
ctx.drawImage(
carImages,
this.x,
this.y,
this.width,
this.height
);
},
jump() {
if (this.y > 40) {
this.y -= 60;
}
},
fall() {
if (this.y < 120) {
this.y += 10;
}
},
};
// 장애물
var boxImages = [new Image(), new Image()];
boxImages[0].src = "/pong.png";
boxImages[1].src = "/pong2.png";
class Box {
constructor() {
this.width = 40;
this.height = 40;
this.x = canvas.width - this.width;
this.y = 155;
// 장애물 이미지 선택
const randomImageIndex = Math.floor(Math.random() * boxImages.length);
this.image = boxImages[randomImageIndex];
}
draw() {
ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
}
}
// spaceBar
var jumpSwitch = false;
let lastSpacePressTime = 0;
document.addEventListener("keydown", function (e) {
if (e.code === "Space") {
const currentTime = Date.now();
const timeSinceLastPress = currentTime - lastSpacePressTime;
if (timeSinceLastPress > 500) {
jumpSwitch = true;
lastSpacePressTime = currentTime;
}
}
});
</script>
3 - 4. 장애물, 도착지 생성 및 Score 관련 Java Script
Destination 클래스에서는 도착지의 속성과 초기 위치를 설정하고, 도착지가 다른 장애물들과 충돌하지 않도록 확인하도록 하였다. 만약 충돌이 발생하면 새로운 도착지를 생성한다.
createBox 함수는 장애물을 생성하고, 생성된 장애물이 다른 장애물이나 도착지와 충돌하지 않게 하였다. 만약 충돌이 있으면 다시 호출되어 장애물을 다시 생성하게 된다
updateScore 함수는 Score를 1씩 증가시키고, 게임화면 좌측 상단에 있는 Score를 업데이트한다.
<script>
// 도착지 이미지 로드
let destinationImage = new Image();
destinationImage.src = "/link.png";
class Destination {
constructor() {
this.width = 100;
this.height = 100;
this.x = canvas.width - this.width; // 장애물과 동일한 위치에서 시작
this.y = 95;
// 새로 생성한 도착지와 다른 장애물들과의 충돌 확인
let isCollision = manyBoxes.some(existingBox => {
return (
this.x < existingBox.x + existingBox.width &&
this.x + this.width > existingBox.x
);
});
// 충돌이 있으면 다시 생성 시도
if (isCollision) {
return new Destination();
}
}
draw() {
ctx.drawImage(destinationImage, this.x, this.y, this.width, this.height);
}
}
let manyBoxes = [];
let destination;
// 장애물 생성 함수
function createBox() {
let box = new Box();
// 새로 생성한 장애물과 다른 장애물들 및 도착지와의 충돌 확인
let isCollisionWithOtherBoxes = manyBoxes.some(existingBox => {
return (
box.x < existingBox.x + existingBox.width &&
box.x + box.width > existingBox.x
);
});
let isCollisionWithDestination =
destination &&
box.x < destination.x + destination.width &&
box.x + box.width > destination.x;
// 충돌이 없으면 배열에 추가, 있으면 다시 생성 시도.
if (!isCollisionWithOtherBoxes && !isCollisionWithDestination) {
manyBoxes.push(box);
} else {
createBox(); // 다시 생성 시도.
}
}
// score
let score = 0;
let scoreInterval;
function updateScore() {
score += 1;
document.querySelector(".score span").textContent = score;
}
</script>
3 - 5. 게임 진행시 반복적으로 실행되는 게임 Frame 관련 Java Script
requestAnimationFrame(frameRun)은 브라우저에게 다음 프레임에서 frameRun 함수를 호출하도록 요청하는 함수로 이를 통해 게임 루프가 지속적으로 실행한다.
if (timer % 10 === 0) 은 매 10프레임마다 스코어를 업데이트한다.
if (Math.random() < 0.005) 부분은 게임에서 무작위로 장애물을 생성하기 위한 확률적인 조건을 나타낸다. Math.random() 함수는 0 이상 1 미만의 값을 반환한다. 즉, 매 프레임에서 Math.random() 함수를 호출하여 0 이상 1 미만의 난수를 생성하고, 이 값이 0.005 미만일 때에만 실행된다. 이렇게 함으로써 매 프레임마다 장애물을 생성할 확률을 매우 낮게 만들고, 게임이 무작위로 장애물을 생성할 수 있다.
if (score == 150 && !isDestinationVisible) 은 도착지 생성 조건을 나타내고 있다. score가 150점 일 때 도착지를 생성하여 화면상에 나타나도록 설정하였다.
if (isDestinationVisible) 은 도착지가 화면에 표시되는 경우 실행되어 도착지의 위치를 업데이트하고, 플레이어가 도착지에 도달하면 게임이 성공으로 처리됩니다.
<script>
let isDestinationVisible = false;
let timer = 0;
let jumpTimer = 0;
let animation;
// 프레임마다 실행하기
function frameRun() {
animation = requestAnimationFrame(frameRun);
timer++;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// score 처리
if (timer % 10 === 0) {
updateScore();
}
// 무작위로 장애물 소환
if (Math.random() < 0.005) {
createBox();
}
// 도착지 생성 조건
if (score == 150 && !isDestinationVisible) {
isDestinationVisible = true;
destination = new Destination();
}
if (isDestinationVisible) {
destination.x -= 2;
destination.draw();
// 도착지에 도달하면 성공 처리
if (electricCar.x + electricCar.width >= destination.x && electricCar.y + electricCar.height >= destination.y &&
electricCar.x <= destination.x + destination.width && electricCar.y <= destination.y + destination.height) {
showGameSuccess();
cancelAnimationFrame(animation);
clearInterval(scoreInterval);
return;
}
}
// x좌표가 0미만이면 제거
manyBoxes.forEach((a, i, o) => {
if (a.x < 5) {
o.splice(i, 1);
}
a.x -= 2;
// 충돌 체크
crash(electricCar, a);
a.draw();
});
// 점프!
if (jumpSwitch == true) {
electricCar.jump();
jumpTimer++;
}
if (jumpSwitch == false) {
if (electricCar.y < 120) {
electricCar.y++;
}
}
if (jumpTimer > 40) {
jumpSwitch = false;
jumpTimer = 0;
}
electricCar.draw();
}
</script>
<script>
// 도착지에 도달하면 게임 성공 팝업 표시
function showGameSuccess() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
cancelAnimationFrame(animation);
clearInterval(scoreInterval);
const totalScore = document.querySelector(".total-score");
totalScore.textContent = `${score}`;
const sun = document.querySelector(".sun");
const gameSuccess = document.querySelector(".game-over");
sun.style.animationPlayState = "paused";
gameSuccess.style.display = "block";
let gameOverMsgElement = document.querySelector('.game-over .pop-up h2');
gameOverMsgElement.textContent = '충전소에 도착했습니다!';
const getCoinsButton = document.querySelector(".get-coins");
getCoinsButton.style.display = "block"; // 버튼 표시
getCoinsButton.addEventListener("click", async function () {
const response = await fetch("/getCoins", {
method: "GET",
});
if (response.ok) {
const message = await response.text();
alert(message);
// 필요하다면 이후에 페이지를 새로고침하거나 다른 동작을 수행하세요.
} else {
console.error("찌리릿 코인을 받는 중 오류가 발생했습니다: " + response.status);
}
});
// "다시하기" 버튼을 숨깁니다.
const replayButton = document.querySelector(".replay");
replayButton.style.display = "none";
}
</script>
4. 어려웠던 점
구현하는데 가장 시간이 오래 걸렸던 기능은 장애물이 생성되는 시간과 간격을 조절 하는 것이었다. 0.001초 단위로 장애물 생성의 변화가 크게 달라지다보니 적당한 시간을 찾기가 어려웠다.
또한 게임을 하다보면 목적지와 장애물이 겹쳐나오거나 목적지를 지나치는 변수가 발생했다. 그래서 처음에 도착지를 지나치면 그 다음 설정한 점수에 도달했을 때도 목적지가 다시 생성되게 구현하고 싶었는데 아무리 코드를 작성해도 목적지가 한번 나오면 그 다음에 나오지 않았다. 결국 다른 방법으로 문제를 해결했는데 목적지인 주유소의 크기를 크게 만들어 목적지 근처에만 도달해도 성공할 수 있도록 수정하였다.
호텔 근처 Russell Square 역에서 나와 처음 마주한 런던 무거운 캐리어 들고 계단 올라올때까지만 해도 한국 돌아가고싶었는데 보자마자 싹 잊어버렸다 이때로 돌아가고싶다
체크인 하고 짐 대충 정리하고 근처 마트 가려고 나왔는데 문이 안잠기면 어떡하라는거야 마침 관리인이 지나가서 말씀드렸더니 기다리라고 하셔서 친구랑 가만히 서있는데 계속 안되는지 사람들이 하나 둘 늘어났다 나중에 한 5명 와있었나 방 안에서 계속 기다리다 결국 안되서 방 바꿔줬는데 바꾼 방이 너무 별로였다 쮀엣
대충 마무리 하고 마트가서 사온 것들 파예 요거트 먹고싶었는데 왜인지 런던에는 없었다 원래 안파나? 저 프로틴 빵은... 파리 넘어갈때 까지 남아있었다
2일차 5월 19일 본격적인 런던 투어
진짜 런던이구나 느꼈던 빨간 2층 버스! 눈 돌릴 때 마다 너무 행복했다 건물 하나하나가 너무 예쁘고 그냥 여기 평생 살고 싶어 나 여기 다시 갈 수 있을까
지하철 타고 와서 걷다보니 저 멀리 보이는 런던에 온 이유 Tottenham Hotspur Stadium 웅장 삐까뻔쩍 그자체
투어 시작하기 전에 일단 굿즈샵 투어 시즌 종료 전이라 그런지 홈 유니폼이 없었다 나 저 간고등어 싫은뒈... 경기날 몇개 풀지 않을까 싶어서 일단 유니폼은 안사고 머플러만 샀다 근데 머플러 사진을 안찍었네
투어시작 ! 나의 행복 나의 사랑 그리고 Son, H-M
나 홈구장 잔디도 밟아봤다 여기서 찍은 사진들 보면 그렇게 행복해 보일 수 가 없음
아프지말고 행축하자
역 앞에 있던 플리마켓 목요일에만 열리는 것 같았다. 여기서 피스타치오 티라미수 사 먹었는데 여긴 진짜 피스타치오에 진심이다 맛을 고를 수 있다? 무조건 피스타치오 레츠고
짐 내려두려고 호텔 가다가 들린 호텔 바로 옆에 있던 Tavistock Square Garden 날씨가 너무 좋았다. 그래서 공원에서 만난 청설모 인생샷도 찍어줌✌️
호텔에서 잠깐 쉬고 나와서 SOHO 갔는데 런던 사람들 여기 다 모였나? 사람이 너무 많아서 가방 꽉 붙잡고 걸은 기억 밖에 안나
유럽 여행 다녀와서 해리포터 정주행 했는데 여행 가기 전에 봤다? 런던에서 캐리어 하나 더 샀다 슬리데린 머플러 왜 안샀어왜 그리고 계속 돌아다니다 갑자기 든 생각 우리 밥 왜 안먹지?
이게 뭐냐구요? 바로 J의 여행 계획표 입니다 전날 침대에 누워서 내일 뭐하지 하면서 계획표 보는데 이렇게 댕강 써져있는게 너무 웃겨서 찍어놓음 그래서 먹으러 갔다
근데 지금 생각하면 왜 런던까지 와서 쌀국수 먹었는지 모르겠는데 한국 사람들이 많이 먹길래... 그래서 그랬지... 근데 지금 생각나는 건 그냥 너무 비쌌다 🫠 근데 단 하나 좋았던 점? 바로 옆에 왕 맛있는 스콘집이 있었다는
밥은 밥이고 빵은 빵이지 여기가 그 유명한 Masion Bertaux 가게 후기 보면 다들 매장에서 차랑 같이 드시던데 매장에 자리도 없고 배도 불러서 일단 포장했다
근데 밥 먹고 커피랑 스콘? 어떻게 참아 날씨도 좋고 호텔 들어가기 싫어서 바로 빵크닉 해버렸다 스콘 딱 처음 먹었을 땐 잉? 했는데 근데 크림이랑 먹으니까 계속 생각나는 맛이다 그래서 파리 가기 전에 또 사먹었다😙
빵크닉하고 호텔 들어가서 몇분 안쉬고 바로 또 나왔다 구경한다고 버스도 안타고 돌아다녔는데 나중에 걸음 수 보니까 4만보정도 걸었나
여기저기 구경하다가 도착한 트라팔가 광장 외국인 아저씨랑 내 친구랑 만담 펼치는데 옆에서 동영상 찍고 있었다 그리고 이때까진 몰랐다 내가 런던에 또 오고 싶은 이유이자 런던하면 바로 생각나는 그게 바로 앞에 있을 줄은 !!
보이세요? 저기 보이세요? 정말 생각지도 못하고 신호등 대기하고 있는데 저 멀리 보이는 빅벤에 신호등 건너지도 않고 사진을 찍었고 내가 제일 좋아하는 사진이 되었다.
빅벤에 점점 가까워지는 중입니다. 빅벤으로 걸어가다가 런던아이도 살짝
지금부터 빅벤 사진만 나옵니다 제가 찍은 빅벤 자랑하고 싶어요
40분 정도 앉아서 빅벤 구경하다 9시 정각에 종소리도 듣고 그냥 앉아서 보는 것 만으로도 너무 행복했다
빅벤 구경 실컷하고 어두워질때쯤 런던아이 보러갔는데 여기 한국사람들 정모 중 맞죠 한국분들 사진 찍어드린 기억밖에 안난다
어쨌든 런던아이 국룰 인증 사진 건졌으니 만족하고 너무 추우니까 호텔로 돌아가겠습니다.
근데 이 사람들 또 걸어감 들어가서 한식 챙겨온거 먹고 바로 기절 했슴미다 2일차 런던여행 마무리 !
첫 화면에 들어오면 멤버십 가입 관련 팝업이 뜬다. '혜택 자세히 알아보기'를 누르면 멤버십 가입 페이지로 이동하고 '오늘 하루 이 창을 열지 않습니다'에 체크한 뒤 닫기를 누르면 쿠키에 정보가 저장이 되어 오늘 하루 팝업 창이 뜨지 않는다.
1-3. 홈 화면
상단에 웹 이름, 로고, 카테고리를 두었고 오른쪽에 사이드바(현재 닫힘), 중간에 피카충전소의 슬로건과 관련 이미지를 배치하였다. 뒷 배경에 번개가 치는 애니메이션 효과를 넣어 전기라는 이미지가 떠올리게 하였다.
1-4. 사이드 바
오른쪽에 번개모양의 이모티콘을 누르면 사이드 바가 열리고 닫힌다. 로그인 전, 후에 생성되는 항목이 다르며 하위 요소가 있는 목록들은 해당 목록에 마우스를 올리면 사이드바가 하나 더 열려 하위 항목들이 나타난다.
1-5. 카테고리 바
5개의 카테고리로 구성되어 있고, 하위 항목이 있는 카테고리일 경우 해당 카테고리에 마우스를 올리면 하위 항목들이 아래로 내려온다.
2. Account Page
2-1. 회원가입
회원가입 시 이용약관 필수사항 동의, 아이디 중복확인, 이메일 본인인증을 완료해야 회원가입 버튼이 활성화 되도록 구현하였다. 자신의 차를 등록하고자 할 때 차량 회사 선택을 하면 회사에 포함 된 차종이 항목으로 뜬다.
아래 관리자로 체크를 하고 가입을 하면 관리자 계정으로 구분되어 일반 User보다 다룰 수 있는 사항이 많아진다.
2-2. 로그인
간단한 로그인 창으로 DB에 저장되어있는 정보로 로그인을 할 수 있다. 로그인을 할 때 정확한 이메일을 입력하지 않으면 형식이 올바르지 않다고 알림창이 뜨고 비밀번호나 아이디를 틀리면 '이메일 또는 비밀번호가 일치하지 않습니다' 라는 알림창이 뜨게 된다. 비밀번호 입력창에 자신의 비밀번호를 입력한 뒤 오른쪽에 눈 모양의 버튼을 누르면 Password처리 되어 보이지 않았던 비밀번호가 현재 자신이 무엇을 입력해 놓았는지 볼 수 있다.
2-3. 비밀번호 찾기
로그인 창 아래의 '비밀번호가 기억나지 않는다면?'을 클릭하면 본인 인증 후 비밀번호를 새롭게 설정할 수 있. 본인인증에 실패하면 '입력하신 정보와 일치하는 사용자를 찾을 수 없습니다.' 라는 알림창이 뜨기 때문에 DB에 있는 계정만이 본인인증에 성공해서 비밀번호를 변경할 수 있다.
2-4. Mypage
로그인 후 마이페이지 탭에 들어가면 회원탈퇴가 가능하다. (회원수정을 오류로 수정 중)
3. Categori Page
3-1. 충전소 찾기
충전소 찾기로 들어오면 왼쪽 사이드바를 이용하여 지정한 반경 이내의 전기차 충전소 위치를 확인 할 수 있고, 카테고리별로 항목을 선택하여 전기차 위치를 볼 수 있다.
3-2. 게시판
게시판은 공지사항, Q&A로 나뉘어져있으며 글쓰기와, 글의 수정 삭제가 가능하다. Q&A는 일반 글쓰기와, 충전소 추가요청, Car정보 추가요청이 따로 나뉘어져 있어서 각각 추가요청을 하고 글을 쓰면 추가 요청 리스트가 보이게 된다.
3-3. 정보마당
정보마당에는 통계정보와 서비스 안내가 있다. 통계정보에는 전기차 현황과 충전기 현황을 볼 수 있도록 나누어 놓았으며, 서비스 안내에는 충전요금, 전기차 충전방식을 볼 수 있도록 나누어 놓았다.
3-4. 미디어
미디어는 전기차 뉴스, 전기차 영상, 전기차 주행거리, 전기차 게임으로 구성 된 하위 항목을 가지고 있다.
3-4-1. 전기차 뉴스
뉴스 API를 이용해 전기차 관련 뉴스를 불러와 보여주고 있으며 네이버 뉴스 링크로 이동이 가능하다.
3-4-2. 전기차 영상
유튜브API를 이용해 전기차 관련 영상을 보여주고 있다. 카테고리를 전기차와 친환경으로 나누어 항목에 따라 다른 영상을 볼 수 있게 하였다.
3-4-3. 전기차 주행거리
유일하게 리액트로 구현한 페이지로 해당 페이지에 들어가면 전기차에 따른 주행거리를 알 수 있으며, 자신이 궁금한 차량을 검색해서 볼 수도 있다.
3-4-5. 전기차 게임
전기차 게임 페이지 들어가면 왼쪽에 게임 관련 설명이 나와있고 오른쪽 미니 게임에서 키보드로 자동차를 움직여 충전소로 가면 본 게임이 시작된다. 만약 자동차가 지도의 가장자리에 닿으면 다음 지도로 전환되게 구현하였다.
본 게임은 장애물을 피해 목적지인 주유소에 도착하면 되는 게임으로 성공시 찌리릿 코인을 지급한다. 찌리릿 코인은 하루에 1번만 받을 수 있으며 게임을 해서 한번 더 받으려고 한다면 오늘은 이미 받았다는 알림창이 뜨게 된다.
3-5. 멤버십
멤버십은 코인교환, 쿠폰함, 구독하기로 구성되어있으며 만약 멤버십에 가입한다면 마이멤버십이라는 하위 항목이 하나 더 보이게 된다.
3-5-1. 코인 교환
게임에서 얻은 코인을 쿠폰으로 교환할 수 있는 페이지이다. 코인 10개당 1개의 무료 충전 쿠폰으로 교환 되며 교환 성공 시 교환 성공 모달이 코인이 없어 교환이 실패 되면 코인이 부족하다는 모달 창이 뜨게 된다.
3-5-2. 쿠폰함
피카충전소 웹에서 얻게 되는 쿠폰을 한번에 볼 수 있는 페이지이다. 사용기간이 나와 있으며 사용하고자 할 때는 사용하기 버튼을 눌러 사용할 수 있다. 사용하기 버튼을 누르면 쿠폰 사용하기 모달이 뜨는데 해당 모달에서 직원확인 버튼을 누르며 해당 쿠폰은 사라지게 된다.
3-5-3. 가입하기
피카 PASS라는 멤버십에 가입 할 수 있도록 멤버십 혜택 보기와 가입하기를 구현해 놓았다. 멤버십에 가입을 한다면 로그인 했을 시 멤버십 카테고리에 마이멤버십이라는 항목이 생기는데 이곳에선 멤버십 종료 날짜를 알 수 있다. 또한 멤버십 가입 혜택인 충전소 근처 편의점, 카페, 음식점의 위치 정보와 해당 매장의 쿠폰을 발급 받을 수 있도록 하였다.
가입하기 페이지가입하기 버튼을 누르면 뜨는 모달 창마이멤버십 페이지
4. Admin
회원가입 시 관리자는 관리자 계정으로 회원가입을 할 수 있다. 관리자로 로그인하게 되면 사이드바에 Admin이라는 항목이 나타나게 되는데 해당 페이지에서 user 수, 구독자 수, 고객문의 수 등을 알 수 있다. 또한 user들의 Charging Q&A, Car Q&A, 발급 된 Coupon 을 한 페이지에서 볼 수 있어 관리하기 쉽게 구현해 놓았다.