본문 바로가기
자바스크립트와 캔버스

바빌론JS - 내 진짜 정체를 알려주지.

지난 시간 카메라 주도권 쟁탈전을 벌이던 NPC군은 자신의 이름을 밝혔는데요.

https://itadventure.tistory.com/698

 

바빌론JS - NPC, 내 이름은 케이!

지난 시간 엄청난 기술들을 선보이며 1위 자리를 다시 탈환한 미니 자동차,https://itadventure.tistory.com/697 바빌론JS - 미니자동차의 마을 탐방지난 시간에는 마을을 돌아다니는 NPC가 주인공이었지만

itadventure.tistory.com

그게 전부가 아니라 자신에게는 엄청난 비밀이 있다고 합니다.
출생의 비밀일까요? :)


소스 정리

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 - 비를 피하고 싶어!

지난 시간에는 케이군의 진짜 정체를 알아보았습니다.https://itadventure.tistory.com/699 바빌론JS - 내 진짜 정체를 알려주지.지난 시간 카메라 주도권 쟁탈전을 벌이던 NPC군은 자신의 이름을 밝혔는데

itadventure.tistory.com