지난 시간 카메라 주도권 쟁탈전을 벌이던 NPC군은 자신의 이름을 밝혔는데요.
https://itadventure.tistory.com/698
그게 전부가 아니라 자신에게는 엄청난 비밀이 있다고 합니다.
출생의 비밀일까요? :)
소스 정리
NPC 케이군의 정체를 밝히기 전에 소스를 정리하고 들어가겠습니다.
일부 변수를 전역변수로, 그리고 기능들을 대부분 함수화한 main.js 소스입니다.
main.js 를 아래 코드로 바꿔주세요.
const canvas = document.getElementById("renderCanvas");
const engine = new BABYLON.Engine(canvas,true);
// 입력 시스템
const deviceInputSystem = new BABYLON.DeviceSourceManager(engine);
// 마우스
const mouseDeviceSource = deviceInputSystem.getDeviceSource(BABYLON.DeviceType.Mouse);
let mesh_car = null;
let mesh_npc = null;
let currentCamera = null;
let npc_name = null;
let advancedTexture = null;
let camera;
let arc_camera;
var createScene = function () {
var scene = new BABYLON.Scene(engine);
// GUI
advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");
// 팔로우 카메라
camera = new BABYLON.FollowCamera(
"FollowCam",
new BABYLON.Vector3(-6, 0, 0),
scene
);
arc_camera = new BABYLON.ArcRotateCamera(
"ArcCam",
BABYLON.Tools.ToRadians(90),
BABYLON.Tools.ToRadians(65),
2, // 카메라 거리
BABYLON.Vector3.Zero(),
scene
);
followCam(scene, null);
// 빛
var light = new BABYLON.HemisphericLight(
"light",
new BABYLON.Vector3(0, 1, 0),
scene
);
// 빛강도
light.intensity = 1.5;
// 마을 부르기
BABYLON.SceneLoader.ImportMeshAsync("", "./", "dwellings.glb")
.then((result) => house_callback(scene, result) );
// 자동차 부르기
BABYLON.SceneLoader.ImportMeshAsync("", "./", "car.glb")
.then((result) => car_callback(scene, result) );
BABYLON.SceneLoader.ImportMeshAsync("", "./", "character01.glb")
.then((result) => npc_callback(scene, result) );
BABYLON.ParticleHelper.CreateAsync("rain", scene, false)
.then((set) => rain_callback(set) );
GUISystem();
// 씬 리턴
return scene;
};
function house_callback(scene, result){
// 기본 집 숨김
scene.getNodeByName("house1_base").setEnabled(false);
scene.getNodeByName("house2_base").setEnabled(false);
}
function car_callback(scene, result){
// 로딩 후 실행할 코드
var mesh = result.meshes[0];
mesh_car = mesh;
mesh.scaling = new BABYLON.Vector3(3, 3, 3);
mesh.position.y = 0.5;
// 팔로우 카메라가 따라갈 대상
camera.lockedTarget = mesh;
// car 를 받아와 맞는 방향으로 회전시켜준다.
const car = scene.getMeshByName("car");
car.rotation = new BABYLON.Vector3(-Math.PI / 2, -Math.PI/2, 0);
car.position = new BABYLON.Vector3(-0.1, 0, 0);
// 경로 정의
const track = [];
track.push(new BABYLON.Vector3( 5.66, 0.00, 7.97 ));
track.push(new BABYLON.Vector3( 1.91, 0.50, 0.84 ));
track.push(new BABYLON.Vector3( 2.05, 0.00, -9.08 ));
track.push(new BABYLON.Vector3( -7.90, 0.00, -7.66 ));
track.push(new BABYLON.Vector3( -9.11, 0.00, -1.84 ));
track.push(new BABYLON.Vector3( -8.83, -0.00, 7.41 ));
track.push(new BABYLON.Vector3( 8.54, 0.00, 8.37 ));
track.push(new BABYLON.Vector3( 7.90, 0.00, -8.97 ));
track.push(new BABYLON.Vector3( 3.94, 0.00, -9.15 ));
track.push(new BABYLON.Vector3( 2.64, 0.00, 0.69 ));
track.push(new BABYLON.Vector3( 6.43, -0.00, 6.89 ));
track.push(new BABYLON.Vector3( 5.66, 0.00, 7.97 ));
// 차와 높이값을 맞춤
track.forEach((number, index)=>{
track[index].y=mesh.position.y;
});
// 경로를 선으로 그림
// const pathLine = BABYLON.MeshBuilder.CreateLines("carpath", {points: track});
// pathLine.color = new BABYLON.Color3(0, 1, 0);
let step = 0.03;
let p = 0;
// 시작 위치로
mesh.position = track[p];
// 랜더 프레임별 실행 코드
scene.onBeforeRenderObservable.add(() => {
mesh.movePOV(0, 0, -step);
let distance =
Math.sqrt( Math.abs( mesh.position.x - track[p].x ) ) +
Math.sqrt( Math.abs( mesh.position.z - track[p].z ) );
if(distance < 0.4)
{
p++;
if(p>=track.length)p = 0;
mesh.lookAt(track[p]);
}
});
}
function npc_callback(scene, result){
var mesh = result.meshes[0];
mesh_npc = mesh;
// NPC 이름
// 검정 배경
const rect1 = new BABYLON.GUI.Rectangle();
rect1.width = "150px";
rect1.height = "40px";
rect1.cornerRadius = 5;
rect1.color = "White";
rect1.thickness = 0;
rect1.background = "black";
rect1.alpha = 0.6; // 투명도
advancedTexture.addControl(rect1);
// 글자
npc_name = new BABYLON.GUI.TextBlock();
npc_name.text = "NPC 케이";
rect1.addControl(npc_name);
rect1.linkWithMesh(mesh_npc); // 메시에 붙이기
rect1.linkOffsetY = -50; // 메시 위로 띄우는 간격
mesh.scaling = new BABYLON.Vector3(0.08, 0.08, 0.08);
mesh.position.y = 0.55;
// 경로 정의
const track = [];
track.push(new BABYLON.Vector3( -9.24, -0.00, -0.42 ));
track.push(new BABYLON.Vector3( 0.18, -0.00, 2.19 ));
track.push(new BABYLON.Vector3( -0.14, 0.00, 4.02 ));
track.push(new BABYLON.Vector3( 3.75, 0.00, 4.12 ));
track.push(new BABYLON.Vector3( 3.75, -0.00, -4.45 ));
track.push(new BABYLON.Vector3( 1.83, -0.00, -5.58 ));
track.push(new BABYLON.Vector3( 0.98, 0.00, 0.95 ));
track.push(new BABYLON.Vector3( -4.11, 0.00, 0.68 ));
track.push(new BABYLON.Vector3( -9.24, -0.00, -0.42 ));
// 캐릭터와 높이값을 맞춤
track.forEach((number, index)=>{
track[index].y=mesh.position.y;
});
// 경로를 선으로 그림
//const pathLine = BABYLON.MeshBuilder.CreateLines("triangle", {points: track});
//pathLine.color = new BABYLON.Color3(1, 0, 0);
let step = 0.005;
let p = 0;
// 캐릭터를 시작 위치로
mesh.position = track[p];
mesh.lookAt(track[p]);
// 랜더 프레임별 실행 코드
scene.onBeforeRenderObservable.add(() => {
mesh.movePOV(0, 0, -step);
let distance =
Math.sqrt( Math.abs( mesh.position.x - track[p].x ) ) +
Math.sqrt( Math.abs( mesh.position.z - track[p].z ) );
if(distance < 0.2)
{
p++;
if(p>=track.length)p = 0;
mesh.lookAt(track[p]);
}
// 마우스 클릭 3차원 벡터 추적
// mousepick_traceVector3d();
});
}
function rain_callback(set){
set.systems[0].updateSpeed = 0.1; // 빗방울 속도
set.systems[0].emitRate = 1000; // 비의 양
set.systems[1].emitRate = 200; // 스플래시 양
set.start();
}
function GUISystem(){
// 패널 UI 추가
const panel = new BABYLON.GUI.StackPanel();
// 가로 중앙 정렬
panel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_CENTER;
// 세로 하단 정렬
panel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
// 하단 여백
panel.paddingBottom = 20;
// 패널을 UI 시스템에 붙임
advancedTexture.addControl(panel);
// 버튼1 추가
const button1 = GUI_add_button(panel, "NPC 따라가기", 150, 40, "white", "green");
button1.onPointerClickObservable.add(() => {
followCam(scene, mesh_npc);
});
// 버튼2 추가
const button2 = GUI_add_button(panel, "차 따라가기", 150, 40, "white", "blue");
button2.onPointerClickObservable.add(() => {
followCam(scene, mesh_car);
});
}
function GUI_add_button(panel, text, width, height, forecolor, background){
const button = BABYLON.GUI.Button.CreateSimpleButton("", text);
button.width = width + "px"; // 가로폭 150픽셀
button.height = height + "px";
button.cornerRadius = 5;
button.color = forecolor;
button.background = background;
panel.addControl(button);
return button;
}
function followCam(scene, mesh){
if(mesh!=null)camera.lockedTarget = mesh;
// 대상과의 거리
camera.radius = -1;
// 카메라의 높이
camera.heightOffset = 0.5;
// 카메라 가속도
camera.cameraAcceleration = 0.005
//최대 가속도
camera.maxCameraSpeed = 10
camera.wheelDeltaPercentage = 0.01; // 마우스휠 속도
camera.minZ = 0; // 근거리 커팅 X
camToggle(scene, camera);
}
function ArcCam(scene, mesh){
if(mesh!=null)arc_camera.lockedTarget = mesh;
// 대상과의 거리
arc_camera.radius = 3;
arc_camera.wheelDeltaPercentage = 0.01; // 마우스휠 속도
arc_camera.minZ = 0; // 근거리 커팅 X
camToggle(scene, arc_camera);
}
function camToggle(scene, camera){
// 카메라 캔버스 연결
if(currentCamera != camera)
{
if(currentCamera!=null) currentCamera.detachControl(canvas);
camera.attachControl(canvas, true);
currentCamera = camera;
scene.activeCamera = camera;
}
}
const scene = createScene();
// 마우스 클릭 추적 3차원 벡터 추출 ( cray )
// 필요할 떄 scene.onBeforeRenderObservable.add 에 넣고
// 메쉬를 클릭, alert 창에서 좌표를 복사할 수 있습니다.
function mousepick_traceVector3d()
{
if (mouseDeviceSource.getInput(BABYLON.PointerInput.MiddleClick)) {
const picking = scene.pick(scene.pointerX, scene.pointerY);
if (picking.hit) {
const Point = picking.pickedPoint;
prompt("복사하세요.", "new BABYLON.Vector3( "
+ Point.x.toFixed(2) + ", "
+ Point.y.toFixed(2) + ", "
+ Point.z.toFixed(2) + " )");
}
}
}
engine.runRenderLoop(function () {
scene.render();
});
window.addEventListener("resize", function () {
engine.resize();
});
약간의 변화는 있습니다. 버튼 글자들이 한글로 바뀌었군요.
변신 버튼 추가
NPC 케이군에게 요청이 들어왔습니다.
'변신' 버튼을 추가하고 자신에게 ArcCamera 로 포커스를 맞춰달라 합니다.
뭐 그대로 해보죠 :)
아래 코드를 찾아
// 버튼2 추가
const button2 = GUI_add_button(panel, "차 따라가기", 150, 40, "white", "blue");
button2.onPointerClickObservable.add(() => {
followCam(scene, mesh_car);
});
그 아래에 버튼을 추가했습니다.
// 버튼3 추가
const button3 = GUI_add_button(panel, "변신", 150, 40, "white", "red");
button3.onPointerClickObservable.add(() => {
ArcCam(scene, mesh_npc);
});
이제 변신 버튼을 클릭하면 카메라가 따라가지는 않고 이런 구도가 됩니다.
진정한 변신
그 다음 추가 코드를 보고 크레이는 깜짝 놀랐습니다.
이것이 진정한 NPC 케이의 정체였단 말인가?
우선 따라해 보시죠 :)
아래 코드를 찾아
let arc_camera;
그 아래 코드를 추가합니다.
let npc_mode = 1; // 1=npc, 2=pc
let inputMap = {};
이어서 아래 코드를 찾아
advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("UI");
키보드 이벤트를 추가합니다.
이 것으로 입력키에 대한 input 배열값이 키 입력에 따라 true 또는 false 로 바뀝니다.
// 키보드 이벤트
scene.actionManager = new BABYLON.ActionManager(scene);
scene.actionManager.registerAction(
new BABYLON.ExecuteCodeAction(
BABYLON.ActionManager.OnKeyDownTrigger,
function (evt) {
inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown";
}));
scene.actionManager.registerAction(
new BABYLON.ExecuteCodeAction(
BABYLON.ActionManager.OnKeyUpTrigger,
function (evt) {
inputMap[evt.sourceEvent.key] = evt.sourceEvent.type == "keydown";
}));
그리고 npc_callback() 함수 안의 랜더 프레임별 실행 코드 를 아래와 같이 변경합니다.
// 랜더 프레임별 실행 코드
scene.onBeforeRenderObservable.add(() => {
switch(npc_mode){
case 1: // npc
mesh.movePOV(0, 0, -step);
let distance =
Math.sqrt( Math.abs( mesh.position.x - track[p].x ) ) +
Math.sqrt( Math.abs( mesh.position.z - track[p].z ) );
if(distance < 0.2)
{
p++;
if(p>=track.length)p = 0;
mesh.lookAt(track[p]);
}
break;
case 2: // PC
let forward_speed = 0.02;
let backward_speed = -0.01;
let turnleft_speed = -Math.PI/90;
let turnright_speed = Math.PI/90;
if (inputMap["w"] || inputMap["W"])
mesh.moveWithCollisions(mesh.forward.scaleInPlace(forward_speed));
if (inputMap["s"] | inputMap["S"])
mesh.moveWithCollisions(mesh.forward.scaleInPlace(backward_speed));
if (inputMap["a"] || inputMap["A"])
mesh.rotate(BABYLON.Axis.Y, turnleft_speed);
if (inputMap["d"] || inputMap["D"])
mesh.rotate(BABYLON.Axis.Y, turnright_speed);
break;
}
// 마우스 클릭 3차원 벡터 추적
// mousepick_traceVector3d();
});
하나만 더 하면 NPC 케이의 정체를 알 수 있습니다.
버튼3의 작동 코드를 찾아
button3.onPointerClickObservable.add(() => {
ArcCam(scene, mesh_npc);
});
아래와 같이 수정합니다.
button3.onPointerClickObservable.add(() => {
ArcCam(scene, mesh_npc);
npc_mode=2;
});
어떤 일이 일어났을까요?
'변신' 버튼을 눌러보면 아무런 변화가 없습니다.
아 한가지 변화는 NPC 케이가 더 앞으로 가지 않는다는 것입니다.
NPC 케이군이 말하고 싶다는 군요. "WASD 키를 눌러봐"
와우~ 키보드 입력에 따라 움직이는 NPC 케이군을 볼수가 있습니다.
"아직 내가 NPC인줄 아나 보네, 나는 PC도 된다구"
※ 참고 : NPC ( Non-Player Character ), PC ( Player Character )
그렇습니다. 사실 NPC 케이군은 전천후 능력으로 사용자가 직접 조정할 수 있는 PC도 될 수 있는 것이었지요.
PC군 케이 멋져요~!
그대로 멈춰라!
몇가지 기능을 더 다듬어 봅시다.
NPC가 PC가 된 후로 움직이지 않아도 제자리 걸음을 하는데요.
움직일 때만 걸음을 걷도록 해봅시다.
아래 코드를 찾아
let inputMap = {};
아래코드를 추가합니다.
let walk_anim = null;
이어서 아래 코드를 찾아
switch(npc_mode){
그 위에 아래 코드를 추가합니다.
walk_anim = scene.getAnimationGroupByName("walk");
다음으로 아래 코드를 찾아
case 2: // PC
들여쓰기 코드를 아래 코드로 싹 바꿔치기합니다.
let forward_speed = 0.02;
let backward_speed = -0.01;
let turnleft_speed = -Math.PI/90;
let turnright_speed = Math.PI/90;
let keydown = false;
if (inputMap["w"] || inputMap["W"]) {
mesh.moveWithCollisions(mesh.forward.scaleInPlace(forward_speed));
keydown = true;
walk_anim.play(true);
}
if (inputMap["s"] | inputMap["S"]) {
mesh.moveWithCollisions(mesh.forward.scaleInPlace(backward_speed));
keydown = true;
walk_anim.play(true);
}
if (inputMap["a"] || inputMap["A"])
mesh.rotate(BABYLON.Axis.Y, turnleft_speed);
if (inputMap["d"] || inputMap["D"])
mesh.rotate(BABYLON.Axis.Y, turnright_speed);
if(keydown == false)
walk_anim.stop();
break;
그러면 캐릭터가 ws 키로 이동할 때만 걸음을 움직입니다.
이름표를 바꿔~
이제 정체를 알았으니 버튼 이름과 캐릭터 이름도 바꾸겠습니다.
아래 코드를 찾아,
const button3 = GUI_add_button(panel, "변신", 150, 40, "white", "red");
button3.onPointerClickObservable.add(() => {
ArcCam(scene, mesh_npc);
npc_mode=2;
});
아래와 같이 변경하면
const button3 = GUI_add_button(panel, "플레이어 변신", 150, 40, "white", "red");
button3.onPointerClickObservable.add(() => {
ArcCam(scene, mesh_npc);
npc_mode=2;
npc_name.text = "플레이어 케이";
});
버튼 이름이 '플레이어 변신'으로,
버튼 클릭시, 이름표도 '플레이어 케이'로 바뀝니다.
반면 NPC 따라가기 버튼을 클릭하면 다시 원상 복구해줘야겠지요?
아래 코드를 찾아
const button1 = GUI_add_button(panel, "NPC 따라가기", 150, 40, "white", "green");
button1.onPointerClickObservable.add(() => {
followCam(scene, mesh_npc);
});
아래와 같이 바꾸면 됩니다.
const button1 = GUI_add_button(panel, "NPC 따라가기", 150, 40, "white", "green");
button1.onPointerClickObservable.add(() => {
followCam(scene, mesh_npc);
npc_mode = 1;
npc_name.text = "NPC 케이";
walk_anim.play(true);
});
전체 코드 따라오시기 힘들거나 실습이 어려우신 분은 크레이의 홈페이지에 마련된 페이지를 참조해 주세요.
http://dreamplan7.cafe24.com/babylon/ex16/
마무~리
NPC 케이의 정체가 사실 PC 였다고 하니, 이제부터는 좀 알아봐줘야 할것 같습니다 :)
아무쪼록 필요하신 분에게 도움이 되셨기를 바라며 오늘도 방문해 주신 모든 분들께 감사드립니다.
다음 게시글 : https://itadventure.tistory.com/701
'자바스크립트와 캔버스' 카테고리의 다른 글
바빌론JS - GUI 에디터! (0) | 2024.07.31 |
---|---|
바빌론JS - 비를 피하고 싶어! (0) | 2024.07.29 |
바빌론JS - NPC, 내 이름은 케이! (0) | 2024.07.25 |
바빌론JS - 미니자동차의 마을 탐방 (2) | 2024.07.22 |
바빌론JS - 경로 우대? 경로 따라 걷는 NPC (2) | 2024.07.21 |