고급 SVG 애니메이션

이 항목에서는 웹 사이트용 SVG 애니메이션을 만드는 과정의 고급 개념에 대해 설명합니다. 이 자습서를 시작하기 전에 기본 SVG 애니메이션, 중급 SVG 애니메이션을 충분히 익히고 HTML 및 JavaScript를 이해해야 합니다.

참고  이 항목에서 제공하는 예제를 보려면 SVG 요소를 지원하는 브라우저(예: Windows Internet Explorer 9 이상)가 필요합니다.

이 항목에서는 중급 SVG 애니메이션에서 설명한 원형 공 경기장을 교육용 2차원 비디오 게임으로 확장합니다.

SVG 튕기기 게임 스크린샷

게임의 목표는 간단합니다. 원형 경기장의 왼쪽에 있는 결승점(바깥쪽 벽의 비어 있는 공간)을 통과하도록 공을 라켓으로 튕기는 것입니다. 공이 라켓에 닿은 후에만 결승점에 들어갈 수 있습니다. 공이 라켓에 닿으면 "핫" 상태가 되며 색이 "콜드" 흰색에서 흰색이 아닌(임의로 선택됨) "핫" 색으로 바뀝니다. 자세한 내용과 게임 플레이 팁을 보려면 게임 개요를 참조하세요.

게임을 시험적으로 해보려면 SVG 공 튕기기를 클릭하세요. 게임 개요에 설명된 대로 페이지에서 아무 곳이나 클릭하면 게임이 시작되고 마우스를 위/아래 세로로 이동하면 라켓이 위/아래로 이동합니다.

지금까지 게임의 모양을 살펴보았으므로 이제 구현된 순서에 따라 게임을 만드는 방법에 대해 설명하겠습니다.

다음 설명에서, 게임 좌표계의 원점은 경기장의 중심에 있지만(표준 데카르트 좌표계처럼) y축은 x축 아래에서 양수이고 x축 위에서 음수라는 점에서, 게임에 사용된 좌표계가 의사 데카르트라는 것을 알고 있으면 도움이 됩니다.

또한, 관련된 수학의 많은 부분을 크게 간소화할 수 있기 때문에 극좌표가 종종 사용된다는 것을 알고 있어야 합니다.

일부 JavaScript 코드가 포함된 극좌표

Figure 1

길이 때문에 다음 예제의 어떠한 소스 코드도 이 항목에 표시되지 않습니다. 대신 "라이브 링크"가 각 예제에 대해 제공됩니다. 예제와 연결된 소스 코드를 보려면 브라우저의 소스 보기 기능을 사용하세요. 예를 들어 Windows Internet Explorer에서는 보려는 소스 코드의 웹 페이지를 마우스 오른쪽 단추로 클릭하고 소스 보기를 클릭하세요. 이 문서의 나머지 내용을 확인하면서 사용 가능한 해당 소스 코드가 있는지 확인하세요.

예제 1 – 정적 프레임워크

 예제 1

이 게임은 정적 게임 프레임워크를 기반으로 작성되었습니다.

예제 2 코드의 스크린샷

Figure 2

이 프레임워크는 다음 태그를 기반으로 합니다.


<body id="gamePage" onload="game.init();">
  <div id="rules" class="roundCorners">
    <p><a href="rules.html" target="_blank">Game Overview</a></p>
  </div>
  <div id="clock" class="roundCorners">
    <p>Seconds Remaining:</p> 
  </div>
  <div id="score" class="roundCorners">
    <p>Score: 0</p>
  </div>  
  <div id="level" class="roundCorners">
    <p>Level: 1</p>
  </div>
  <div id="messagingBox" class="roundCorners"> 
    <h3>SVG Ball Bounce</h3>
    <p>Dare to get your balls in the goal, to score!</p>
  </div>
  <div id="arenaWrapper">
    <svg id="arenaBox" width="800px" height="800px" viewBox="0 0 800 800"> 
      <g id="arena" transform="translate(400, 400)">
        <circle id="floor" cx="0" cy="0" /> 
        <path id="wall" /> 
        <circle id="post" cx="0" cy="0" />         
        <rect id="paddle" x="300" y="-50" width="8" height="100" rx="2" ry="2"/>
      </g>
    </svg>
  </div>
</body> 


이 태그는 상대적으로 간단하지만 다음 항목이 유용할 수 있습니다.

  • 세 가지 Time, LevelScore 상자의 콘텐츠(<div> 요소를 사용하여 구현)는 프로그래밍 방식으로 변경되고 네 가지 모두 position:absolute CSS를 사용하여 웹 페이지의 네 모서리에서 상자의 위치를 지정합니다.
  • <g id="arena" transform="translate(400, 400)">의 변환 특성에 의해 기본 SVG 좌표계는 의사 데카르트 좌표계로 수정됩니다(앞에 설명된 대로).

JavaScript 관점에서 게임의 기본적인 논리 흐름은 다음과 같습니다.

  • 페이지가 완전히 로드되면 Game.prototype.init가 호출되어, 경기장을 그리고(Arena.prototype.draw) 게임을 시작합니다(Game.prototype.start).
  • Game.prototype.startrequestAnimationFrame을 호출하여 작업의 대다수를 수행할 Game.prototype.play 콜백 함수를 설정하고 setInterval을 호출하여 오른쪽 위의 상자에 있는 게임 시계를 1초에 한 번씩 업데이트하는 Game.prototype.clock 콜백 함수를 설정합니다. requestAnimationFramesetInterval에 의해 호출된 함수가 올바른 this 포인터에 액세스하도록 JavaScript 닫기(예: var gameObject = this;)를 만들기 위한 Game.prototype.start 내 요구 사항을 확인하세요.
  • Game.prototype.start로 인해 Game.prototype.playrequestAnimationFrame을 통해 16.7밀리초(즉, 약 60FPS) 간격으로 호출됩니다. Game.prototype.play는 현재 빈 함수이지만 결국 기본 게임 루프를 포함하게 됩니다.
  • Game.prototype.clock은 1초에 한 번씩 호출되고 나머지 게임 시간을 줄입니다. 게임 시간이 constants.warningTime보다 적거나 같은 경우 원형 게임 경기장의 배경이 빨간색으로 깜박입니다. 먼저 경기장 바닥을 빨간색으로 설정하고, 짧은 시간 후 바닥 색을 다시 흰색으로 변경하기 위해 또 다른 호출 함수(즉, Arena.prototype.defaultFloorColor)를 설정하여 이 효과를 얻게 됩니다. 해당 콜백 함수는 한 번만 호출되므로 이를 위해 setTimout이 사용됩니다. 이것이 바로 1초에 한 번씩 경기장 바닥을 펄스하기 위해 필요한 동작입니다.

마지막으로 게임 생성자 함수의 마지막 네 줄을 주의하세요.


function Game(level) {
  this.paused = true; 
  this.ended = false; 
  this.level = level; 
  this.score = 0; 
  this.time = constants.playTime; 
  this.arena = new Arena(this); 
  this.goal = new Goal(this); 
  this.paddle = new Paddle(this);
  this.balls = new Balls(this); 
}

이러한 줄에서는 게임 개체 자체에 대한 공, 라켓, 결승점 및 경기장 개체 액세스를 허용합니다. 이는 하나의 개체가 다른 개체를 액세스하는 경우 유용하게 됩니다. 예를 들어 Arena.prototype.draw에서 다음과 같이 시계를 업데이트합니다.


updateClock(this.game.time);

여기에서 경기장 개체는 게임 개체의 시간 속성을 액세스하기 위해 게임 개체 속성을 액세스합니다.

예제 2 – 라켓 이동

 예제 2

이 예제에서는 위쪽 화살표 키 또는 아래쪽 화살표 키를 누르거나 마우스를 세로로 이동하면 라켓이 이동됩니다.

시작하려면, 예제 1에서 사용된 모든 JavaScript 도우미 함수가 외부 파일 helpers.js(따라서 예제 2에서 사용된 <script src="helpers.js" type="text/javascript"></script>)로 이동되었음을 알고 있어야 합니다. helpers.js에서 추가 도우미 함수를 볼 수도 있습니다. 이러한 함수는 이후 예제에서 사용됩니다.

참고  외부 JavaScript 파일의 내용을 보려면 브라우저에서 JavaScript 파일의 해당 경로를 지정하고 이 파일을 열거나 저장합니다. 예를 들어 helpers.js를 보려면 <script src="helpers.js" type="text/javascript"></script>helpers.js과 동일한 디렉터리에 있는 example2.html에서 봅니다. 따라서 example2.html이 http://samples.msdn.microsoft.com/Workshop/samples/svg/svgAnimation/advanced/example2.html에 있으면, helpers.js를 열기 위해 필요한 경로는 http://samples.msdn.microsoft.com/Workshop/samples/svg/svgAnimation/advanced/helpers.js입니다. JavaScript 파일을 보려면 우선 파일을 로컬에 저장하고 메모장(또는 유사한 것)에서 열어야 할 수 있습니다.

예제 1에서는 사용자 입력 없이 게임이 시작됩니다. 예제 2에서는 위 또는 아래 화살표 키를 누르거나 마우스를 클릭해야만 게임이 시작됩니다. 새로운 이 기능의 기본 논리는 다음과 같습니다.

페이지가 완전히 로드된 후에만 Game.prototype.init가 다음 이벤트 핸들러를 설정합니다.


window.addEventListener('keydown', processKeyPress, false);
window.addEventListener('click', processMouseClick, false);

helpers.js로 이동하면 함수 processKeyPressprocessMouseClick 모두가 전역 변수 game에 직접 액세스한다는 것을 알 수 있습니다. 이러한 함수는 어떤 의미에서는 논리적으로 게임 외부에 있으므로 도우미 함수(게임 메서드와 반대로)로 만들어져 있습니다.

processKeyPress는 단순합니다. 올바른 키를 누르면 Paddle.prototype.keyMove를 호출하며, 이에 따라 라켓이 올바른 방향(즉, 위나 아래)으로 constants.paddleDy 단위만큼 이동합니다. Paddle.prototype.keyMove는 또한 라켓이 바깥쪽 경기장 벽 너머로 가지 않도록 합니다. 이 동작은 피타고라스의 정리를 사용하여 적용됩니다.

최대 라켓 이동을 결정하기 위해 사용된 피타고라스의 정리

Figure 3

그림 3에는 패들의 최대 높이를 제공하는 r2 = x2 + y2 ⇒ y2 = r2 – x2 ⇒ y = √(r2 – x2)가 표시됩니다. 따라서 다음과 같이 이동할 수 있습니다.


var maxY = Math.sqrt( (constants.arenaRadius * constants.arenaRadius) - (paddle.x.baseVal.value * paddle.x.baseVal.value) );

processMouseClick으로 이동합니다. 마우스를 클릭할 때 이 함수의 주요 목적은 마우스 이동 이벤트 수신기(이 경우 Game.prototype.mouseMove)를 추가하는 것입니다. Game.prototype.mouseMove는 마우스의 현재 수직 위치를 기반으로 다음을 통해 라켓을 위아래로 이동합니다.


paddle.y.baseVal.value = evt.pageY - arenaTransformForY - constants.paddleFudgeFactor;

여기에서 중요한 문제는 마우스의 Y 위치(evt.pageY)와 라켓의 Y 위치( (paddle.y.baseVal.value)는 서로 다른 좌표계에 있다는 것입니다. 우선 의사 데카르트 좌표계를 만드는 데 적용된 변환의 Y 구성 요소를 빼서 마우스의 Y 위치를 게임의 의사 데카르트 좌표계로 변환합니다.


<g id="arena" transform="translate(400, 400)">

Paddle.prototype.mouseMove에서 이는 다소 수수께끼 같은 줄을 통해 적용됩니다.


var arenaTransformForY = arena.transform.baseVal.getItem(0).matrix.f;

이 줄은 이전 <g> 태그에서 두 번째 400을 가져옵니다. 마우스 클릭에 따라 라켓을 가운데에 놓는 경험적으로 파생된 상수를 이 400에서 뺍니다. 이제 마우스가 세로로 이동하면 라켓도 이동합니다.

예제 3 – 공 위치

 예제 3

다음 단계에서는 공을 만들고, 아무것도 서로 겹치지 않거나 라켓과 겹치지 않는 "도넛" 모양 경기장 내에 공의 위치를 지정하고 이 공을 DOM(화면에 나타나게 함)에 추가합니다. 이 동작은 다음 메서드를 사용하여 적용됩니다.


Balls.prototype.place = function() {
  this.create(); 
  this.positionInArena(); 
  this.appendToDOM(); 
}

이 코드가 호출하는 세 가지 메서드는 상대적으로 간단하며 다음과 같은 예외가 있을 수 있습니다.

  • Balls.prototype.create에서는 도우미 함수 getRandomArenaPosition(ballElement.r.baseVal.value)가 극좌표를 사용하여 경기장에 공의 위치를 지정합니다.

    
    function getRandomArenaPosition(ballRadius) {
      var p = new Point(0, 0); // This constructor defined in vector.js
      var r = constants.arenaRadius;
      var allowableRandomRadius; 
      var randomTheta; 
      
      allowableRandomRadius = getRandomInteger(constants.postRadius + ballRadius, r - ballRadius);
      randomTheta = getRandomReal(0, 2*Math.PI);
      
      p.x = allowableRandomRadius * Math.cos(randomTheta);
      p.y = allowableRandomRadius * Math.sin(randomTheta); 
    
      return p;
    }
    
    

    다음 다이어그램은 이 코드 예제를 설명하는 데 도움이 됩니다.

    극좌표를 사용하여 경기장 내에서 공 위치 지정

    Figure 4

    그림 4에서 a = constants.postRadius, b = ballRadius, r = r(바깥쪽 경기장 벽의 반지름) 및 θ = randomTheta입니다. allowableRandomRadius는 a + b보다 크거나 같고 r – b보다 작거나 같게 임의로 선택됩니다. randomTheta는 0과 2π라디안(360도) 사이에서 임의로 선택됩니다. 그러면 그림 4의 점(p.x, p.y)에서 제안한 대로 안쪽 점선 원과 바깥쪽 점선 원 사이에 있는 임의의 점(r, θ)이 제공됩니다(그림 1 참조). 그런 다음 점(r, θ)이 표준 수식 x = r cos(θ) 및 y = r sin(θ)을 사용하는 직사각형 좌표로 변환됩니다.

    vector.js예제 3에서 소개되었습니다. getRandomArenaPositionpoint 형식의 p 개체(특히 function Point(x_coordinate, y_coordinate))를 반환하기 때문입니다. 또한 vector.js에는 이후 예제에서 사용되는 일반적인 벡터 작업에 대한 구현이 포함되어 있습니다. 이러한 일반적인 벡터 작업에 대한 자세한 내용은 벡터를 참조하세요.

  • Balls.prototype.positionInArena에서는 지정된 공이 라켓과 충돌했는지 결정하는 Paddle.prototype.hasCollided가 호출됩니다. 다음 코드 예제에서 사용된 기술은 다음과 같습니다.

    1. 공이 라켓과 충돌하지 않았는지 감지하고 실제로 라켓과 충돌하지 않은 경우 false를 반환합니다.
    2. 공의 중심이 모서리 영역 내에 있지 않도록 라켓의 평평한 표면 중 하나에 공이 충돌했는지 감지하고, 그러한 경우 true를 반환합니다.
    3. 피타고라스의 정리를 사용하여 라켓 모서리 중 하나에 공이 충돌했는지 감지하고 그러한 경우 true를 반환합니다.

    다음 코드 예제에서 ball_cx(ball), ball_cy(ball)ball_r(ball)은 각각 공 중심의 X 좌표, 공 중심의 Y 좌표 및 공의 반지름을 반환합니다.

    
    Paddle.prototype.hasCollided = function(ball) {
    /* 
    Returns true if a ball has collided with the paddle, false otherwise.
    */    
      var paddle = document.getElementById('paddle'); // Needed for Firefox. Not needed for IE or Chrome.
      var p = new Object(); // To save on typing, create a generic object to hold assorted paddle related values ("p" stands for "paddle").
    	
      p.x = paddle.x.baseVal.value; // The x-coordinate for the upper left-hand corner of the paddle rectangle.
      p.y = paddle.y.baseVal.value; // The y-coordinate for the upper left-hand corner of the paddle rectangle.
      p.w = paddle.width.baseVal.value; // The width of the paddle rectangle.
      p.h = paddle.height.baseVal.value; // The height of the paddle rectangle.
    	
      p.delta_x = Math.abs( ball_cx(ball) - p.x - p.w/2 ); // The distance between the center of the ball and the center of the paddle, in the x-direction.
      p.delta_y = Math.abs( ball_cy(ball) - p.y - p.h/2 ); // The distance between the center of the ball and the center of the paddle, in the y-direction.
    	
      // See if the ball has NOT collided with the paddle in the x-direction and the y-direction:  */ 
      if ( p.delta_x > (p.w/2 + ball_r(ball)) ) { return false; } 
      if ( p.delta_y > (p.h/2 + ball_r(ball)) ) { return false; } 
    	
      // See if the ball HAS collided with the paddle in the x-direction or the y-direction:  */ 
      if ( p.delta_x <= (p.w/2) ) { return true; } 
      if ( p.delta_y <= (p.h/2) ) { return true; }
    	
      // If we've gotten to this point, check to see if the ball has collided with one of the corners of the paddle:  */    
      var corner = new Object(); // A handy object to hold paddle corner information.
      corner.delta_x = p.delta_x - p.w/2; 
      corner.delta_y = p.delta_y - p.h/2;
      corner.distance = Math.sqrt( (corner.delta_x * corner.delta_x) + (corner.delta_y * corner.delta_y) );  
      return corner.distance <= ball_r(ball);
    }
    
    

예제 4 – 공 이동

 예제 3의

이제 다음 문제를 해결해야 하므로 공 이동에는 여러 가지 새로운 코드가 필요합니다.

  • 다른 공과 충돌하는 공.
  • 경기장과 충돌하는 공(안쪽 및 바깥쪽 벽 모두).
  • 라켓과 충돌하는 공.

다른 공과 충돌하는 공.

공 사이의 충돌은 중급 SVG 애니메이션에서 예제 4를 통해 설명합니다. 자세한 내용은 충돌 반응(PowerPoint)의 "충돌을 통해 이동 발생" 섹션을 참조하세요.

경기장과 충돌하는 공.

공가 경기장 사이의 충돌은 중급 SVG 애니메이션에서 예제 5를 통해 설명합니다. 하지만 while ( this.hasCollided(ball) ) 내의 Arena.prototype.processCollision 루프에 대한 약간의 변경 사항이 있습니다. 해당 내용은 이 항목 뒷부분의 디버깅 섹션에 설명되어 있습니다. 또한 다음과 같이 공이 안쪽 또는 바깥쪽 경기장에 충돌하는 경우 Arena.prototype.hasCollidedtrue를 반환하기 위해 약간 확장되었습니다.


return (d + r >= constants.arenaRadius) || (d - r <= constants.postRadius);

라켓과 충돌하는 공.

공이 라켓과 만드는 각도와 관계없이 다음 그림에서와 같이 공이 항상 같은 고정된 각도에서 튕긴다는 면에서 공과 라켓과의 충돌은 상대적으로 명확합니다.

튀어 오르는 고정된 각도 라켓 공

Figure 5

이 고정된 튕겨 오른 후 각도(Vout)는 공이 라켓에 원래 충돌한 지점의 함수이며 간단한 선형 방정식을 사용하여 계산됩니다.

튀어 오르는 고정 각도 라켓 공에 사용되는 선형 방정식

Figure 6

x-축은 맨 위가 0이고 맨 아래가 paddle.height.baseVal.value에서 나타나는 라켓의 세로 길이를 나타냅니다. y-축은 공이 라켓(세로 표면)에 튕길 때의 각도를 나타냅니다.

선형 수식을 함수 ƒ로 표현하는 경우 그림 6에서 ƒ(0) = 45입니다. 이 경우 공이 라켓의 위쪽(x = 0)에 맞고, 들어오는 각도와 상관없이 45도(y = 45)로 꺾이게 됩니다(그림 5에 표시됨).

마찬가지로, ƒ(paddle.height.baseVal.value) = -45입니다. 여기서는 공이 라켓의 아래쪽에 맞고 -45도로 꺾입니다.

공이 라켓의 중심에 충돌하는 경우 x = paddle.height.baseVal.value/2이고 ƒ(x) = 0입니다. 즉, 공이 정확히 0도(가로)로 라켓으로부터 방향을 바꾸게 됩니다.

선형 방정식 ƒ는 공이 라켓에 충돌하는 경우 오른쪽에서 오는 것으로 가정합니다. 공이 왼쪽에서 오고 라켓의 맨 위 또는 맨 아래에 충돌하는 경우 다음 그림에 표시된 대로 공의 방향이 바뀝니다.

라켓의 오른쪽 모서리에서 튕겨 나가는 공

Figure 7

다음과 같이 ƒ는 Paddle.prototype.verticalBounce에서 구현되고 이 코드는 공이 왼쪽 또는 오른쪽에서 라켓에 충돌하는지 여부를 고려합니다.


if (ball.v.xc >= 0) // The ball has struck the left (vertical) side of the paddle.
  uAngle = 180 - uAngle;

올바른 굴절 각도가 알려지면 단위 벡터 u는 이 각도에서 계산되고 공의 들어오는 속도 벡터 Vin의 규모(그림 5 참조)는 공의 새로운 나가는 속도 벡터 Vout를 생성하는 단위 벡터로 전환됩니다. 즉, u는 계산된 단위 벡터이고 Vin는 들어오는 속도 벡터인 경우 나가는 속도 벡터 Vout = |Vin|u입니다.

이 섹션을 마무리하면서 기본 게임 루프에 있어 다음과 같은 사실을 기억하시기 바랍니다.


Game.prototype.play = function() {
  for (var i = 0; i < this.balls.list.length; i++) 
  {
    this.balls.list[i].move(); 
    this.balls.processCollisions(this.balls.list[i]); 
    this.paddle.processCollision(this.balls.list[i]);        
    this.arena.processCollision(this.balls.list[i]); 
  }      
  var that = this; // Preserves the correct "this" pointer.
  this.requestAnimationFrameID = requestAnimationFrame(function () { that.play(); });
}

공 목록의 모든 공에 대한 알고리즘은 간단합니다.

  1. 공을 약간만 이동합니다.
  2. 공 간의 충돌을 처리합니다.
  3. 공과 라켓 간의 충돌을 처리합니다.
  4. 공과 경기장 간의 충돌을 처리합니다.

Game.prototype.startGame.prototype.playrequestAnimationFrame 호출로 인해 전체 게임을 제어하는 이 기본 루프가 16.7밀리초(즉, 약 60FPS) 간격으로 발생합니다.

또한 Game.prototype.start는 위쪽 또는 아래쪽 화살표를 누르거나 마우스를 클릭하여 게임을 시작하는 경우에만 호출됩니다(processKeyPressprocessMouseClickhelpers.js 참조).

예제 5 – 점수 계산 및 수준 높이기

 예제 5

게임에 추가할 마지막 주요 구성 요소는 점수 계산 및 수준 높이기입니다.

점수 계산

게임 개요에 나와 있듯이, 공이 처음 라켓에 맞을 때까지(그리고 그 후 경기장 벽에 너무 많이 튕기지 않을 때까지) 점수를 계산할 수 없습니다. 어느 공이 핫(결승점에 들어갈 수 있음)이고 어느 공이 콜드(결승점에 들어갈 수 없음)인지 추적하기 위해 각각의 공 개체에 사용자 지정 속성 hotCount를 추가합니다. 공이 라켓에 충돌하고 ball.hotCount = constants.hotBouncesPaddle.prototype.processCollision로 설정될 때까지 이 공 속성은 정의되어 있지 않습니다. 공기 경기장 벽에 맞으면 이 속성이 줄어듭니다. 특정 공이 남긴 핫 튕기기 횟수에 대한 시각 신호를 제공하기 위해 hotCount도 사용됩니다. 다음과 같이 공이 처음으로 라켓에 닿으면 흰색이 채워진 "콜드" 색상에서 흰색이 아닌 다른 임의의 색상이 채워진 "핫" 색상으로 Paddle.prototype.processCollision에서 바뀝니다.


if ( !ball.hotCount )
  ball.style.fill = getRandomColor(); 

여기에서 ball.hotCountundefined인 경우 공은 콜드로 간주됩니다. 이는 공이 처음으로 만들어진 경우(!undefined가 JavaScript에서 true임) 또는 ball.hotCount가 0인 경우(공이 경기장 벽에 constants.hotBounces 이상 튀긴 경우 발생)입니다.

다음과 같이 경기장 벽에 튕길 때마다 공의 hotCount 및 불투명도가 Arena.prototype.processCollision에서 감소합니다(결국 0이 될 때까지).


if (ball.hotCount > 0) {
  --ball.hotCount; 
  updateMessagingBox("Ball " + ball.i + ": " + ball.hotCount + " bounces left to score.");

  ball.style.fillOpacity = ball.hotCount / constants.hotBounces;
  if (ball.style.fillOpacity == 0)
    ball.style.fill = constants.coldBallColor; 
}

hotCount 속성별 해당 항목은 공이 결승점에 들어갈 수 있는지 여부를 결정하는 데에도 사용됩니다.


Goal.prototype.processCollision = function(ball) {
  if ( this.hasCollided(ball) && ball.hotCount > 0 && !ball.inactive) {
    updateMessagingBox("Score! (ball " + ball.i + " gone)");
    ++this.game.score; 
    updateScoreBox(this.game.score); 
    ball.poof(); 
    ball.inactive = true
    if ( this.game.balls.allInactive() ) 
      this.game.nextLevel(); 
  }      
}


수준 높이기

수준 간 이동을 하면 공 목록의 공 수가 줄어듭니다(수준별 공 한 개씩). 여러 개의 공이 있는 수준(수준 2 이상)에서 공이 결승점에 들어가는 경우 플레이에서 이 공을 제거해야 합니다. 원래 방법은 공 목록에서 점수를 획득하는 공을 제거하는 것이었지만 이렇게 하면 처리하기 어려운 코드 동기화 문제가 발생했습니다. 많은 코드를 재작성하는 문제를 방지하기 위해서는 점수를 획득하는 각 공을 비활성으로 표시하고, 이러한 공을 플레이 영역으로부터 바깥쪽 경기장 벽 외부로 이동한 다음 속도 및 반지름을 0으로 적절하게 설정하여 제거할 수 있습니다. 이는 앞에 표시된 대로 ball.poof()에서 담당합니다. 이 방법을 사용하는 경우 여러 게임 플레이 루프 내에 추가 확인이 있습니다(이러한 루프를 찾으려면 JavaScript 코드 내에서 "inactive" 검색).

공 목록이 비활성 공으로만 구성되고 시계에 남은 시간이 있는 경우 다음 수준으로 이동하고 시계를 재설정할 때라는 것을 알 수 있습니다. Goal.prototype.processCollision은 이 상태를 감지하기 위한 첫 번째 방법이기 때문에 이전 코드 조각의 마지막 두 줄에 표시된 대로 이 코드를 여기에 넣습니다.

예제 6 – 유동적 레이아웃

 예제 6

게임에 추가할 마지막 기능은 "유동적 레이아웃"입니다. 유동적 레이아웃을 구현하는 경우, 브라우저의 창 크기가 변경되면 게임의 크기도 그에 따라 변경됩니다. 그러면 더 작은 화면 또는 해상도의 장치에서 게임을 더 쉽게 실행할 수 있습니다.

예제 5Paddle.prototype.mouseMove 메서드에서는 두 개의 좌표계를 다룹니다.

  • 화면 좌표 - 화면의 마우스 위치와 관련된 좌표계(원점은 브라우저 창의 왼쪽 위 구석).
  • 경기장 좌표 - 다음 태그로 정의된 게임 경기장과 관련된 좌표계:
    
    <svg id="arenaBox" width="800px" height="800px" viewBox="0 0 800 800"> 
      <g id="arena" transform="translate(400, 400)">
        <circle id="floor" cx="0" cy="0" /> <!-- The arena floor -->
        <path id="wall" /> <!-- The arena wall less the goal hole. -->
        <circle id="post" cx="0" cy="0" /> <!-- The central post in the middle of the arena. -->
        <rect id="paddle" x="300" y="-50" width="8" height="100" rx="2" ry="2"/>
        <!-- Ball circle elements are appended here via JavaScript. -->          
      </g>
    </svg>
    
    

Paddle.prototype.mouseMove 내에서, paddle.y.baseVal.value = evt.pageY - arenaTransformForY - constants.paddleFudgeFactor를 사용해 마우스의 수직 위치 evt.pageY(화면 좌표)를 경기장(라켓 포함)과 관련된 좌표계에 매핑합니다. 경기장 크기는 고정되어 있으므로(800x800), arenaTransformForY(경기장 좌표)의 의미가 evt.pageY와 관련하여 변하지 않는다는 점에서 이 좌표 변환 "hack"을 빠져나갈 수 있습니다. 그러나 레이아웃이 유동적이면 더 이상 이러한 가정을 할 수 없습니다. 대신, 한 좌표계(화면 좌표)에서 다른 좌표계(경기장 좌표)로 변환하기 위한 일반적인 방법이 필요합니다.

다행히 SVG는 이를 위한 비교적 쉬운 방법을 제공합니다. 이 방법은 SVG 좌표 변환에서 다루는데, 이를 이해해야 계속 진행할 수 있습니다. 이러한 이해를 기반으로, 예제 5를 다음과 같이 변경하면 유동적 버전인 예제 6이 만들어집니다.

CSS 수정

base.css에 다음의 백분율 기반 값이 추가되었습니다.


html {
  padding: 0;
  margin: 0;
  height: 100%; /* Required for liquidity. */  
}

body {
  padding: 0;
  margin: 0;
  background-color: #CCC; 
  height: 100%; /* Required for liquidity. */    
}

body#gamePage svg {
  margin-top: 0.8%; /* Used to provide a liquid top margin for the SVG viewport. */
  min-height: 21em; /* Don't let the playing arena get preposterously small. */
}

body#gamePage #arenaWrapper {
  text-align: center;
  height: 80%; /* Required for liquidity. */  
}

기본적으로, htmlheight: 100%는 페이지가 항상 브라우저의 창 크기가 되도록 보장합니다. bodyheight: 100%는 페이지의 내용이 항상 컨테이너(html 요소)의 크기가 되도록 보장합니다. 설명에 나와 있듯이 body#gamePage svgmargin-top: 0.8%는 SVG 뷰포트를 위한 유동적인 위쪽 여백을 제공하고, body#gamePage #arenaWrapperheight: 80%는 유사한 아래쪽 여백을 제공합니다.

태그 수정

svg 요소에서, widthheight 특성 값을 800에서 100%로 다음과 같이 변경합니다.


<svg id="arenaBox" width="100%" height="100%" viewBox="0 0 800 800">

이것과 이전 CSS 변경 사항이 함께 작동하여 SVG 요소의 뷰포트를 브라우저의 창 크기로 변경할 수 있습니다.

JavaScript 수정

helpers.js에 좌표 변환 함수가 추가되었습니다.


function coordinateTransform(screenPoint, someSvgObject) {
  var CTM = someSvgObject.getScreenCTM();

  return screenPoint.matrixTransform( CTM.inverse() );
}

SVG 좌표 변환에서 설명했듯이, 이 함수는 마우스의 위치(screenPoint)를 someSvgObject와 관련된 좌표계에 매핑합니다. 이 경우 이 좌표계는 경기장(따라서 라켓)의 좌표계입니다.

마지막으로, example6.html에서 Paddle.prototype.mouseMove가 다음과 같이 수정되었습니다(설명 참조).


Paddle.prototype.mouseMove = function(evt) {      
  if (this.game.ended || this.game.paused)
    return;
        
  var paddle = document.getElementById('paddle'); 
  var arena = document.getElementById('arena'); 
  var maxY = Math.sqrt( (constants.arenaRadius * constants.arenaRadius) - (paddle.x.baseVal.value * paddle.x.baseVal.value) );
                  
  var arenaBox = document.getElementById('arenaBox');
  var point = arenaBox.createSVGPoint(); // Create an SVG point object so that we can access its matrixTransform() method in function coordinateTransform().
  point.x = evt.pageX; // Transfer the mouse's screen position to the SVG point object.
  point.y = evt.pageY;
      
  point = coordinateTransform(point, arena); // Transform the mouse's screen coordinates to arena coordinates.

  var paddleHeight = paddle.height.baseVal.value; 
  paddle.y.baseVal.value = point.y - (paddleHeight / 2); // Change the position of the paddle based on the position of the mouse (now in arena coordinates).
      
  var paddleTop = paddle.y.baseVal.value; 
  if (paddleTop <= -maxY) {
    paddle.y.baseVal.value = -maxY;
    return; 
  }
      
  var paddleBottom = paddleTop + paddleHeight; 
  if (paddleBottom >= maxY) {
    paddle.y.baseVal.value = maxY - paddleHeight; 
    return;
  }             
}

라켓은 경기장의 좌표계를 사용하므로, coordinateTransform 도우미 함수를 호출하여 먼저 마우스의 화면 좌표 위치를 경기장 좌표계 내의 유사한 위치로 변환합니다. 마우스의 위치를 변환하면 마우스의 현재 위치를 기반으로 라켓의 위치를 변경하는 일이 쉬워집니다. paddle.y.baseVal.value = point.y - (paddleHeight / 2). paddleHeight/2 상수는 라켓이 중앙으로부터(위로부터가 아니라) 이동하도록 보장합니다.

디버깅

SVG 공 튕기기(SVG Ball Bounce)는 Internet Explorer에서 개발되었으며 IE의 F12 개발자 도구는 JavaScript 버그를 추적하고 해결하는 데 매우 유용하였습니다.

예를 들어 발생한 보다 복잡한 버그 중 하나는 특정 위치에서이긴 하지만 동시에 공이 라켓과 바깥쪽 경기장 벽에 충돌하는 경우 발생하는 표면상 임의의 게임 잠금이었습니다.

이 문제는 while, Arena.prototype.processCollisionPaddle.prototype.processCollision의 "hacky"(및 잠재적으로 무제한) SVGCircleElement.prototype.collisionResponse 루프에서 수행하는 작업과 관련되어 있는 것 같았습니다.

F12 개발자 도구 콘솔 창에서 출력을 모니터링하기 위해 기본 게임 루프(F12Log())뿐만 아니라 각각의 주의 대상 while 루프에서 Game.prototype.play에 대한 호출을 하여 게임이 잠긴 때와 잠긴 위치가 정확히 확인되었습니다. 이러한 F12Log() 호출은 해당 코드에 계속 있지만 주석 처리됩니다("F12Log" 검색).

몇 분 동안 게임을 한 후 게임이 잠겼고 해당 코드가 while 내의 주의 대상 Arena.prototype.processCollision 루프에서 사실상 무제한 루프로 들어간 것이 분명했습니다


while ( this.hasCollided(ball) ) {
  F12Log("In Arena.prototype.processCollision while loop...");
  ball.move(); // Normally move the ball away from the arena wall.
}

분명히 특정 상황에서 ball.move()의 효과가 의도된 대로 작동하지 않았으며 대신 this.hasCollided(ball)true로 되게 하여 무제한 루프가 발생하게 했습니다. 다음 코드 예제에 표시된 바와 같이, 사용되는 간단한 솔루션은 이 "무제한 루프"를 감지한 다음 이 상황에서 문제가 있는 공을 "제거"하는 것입니다.


var loopCount = 0;
while ( this.hasCollided(ball) ) {
  // F12Log("In Arena.prototype.processCollision while loop...");
  if (++loopCount > constants.arenaDeadlock) 
    ball.moveTowardCenter(); 
  else
    ball.move(); 
}

여기에서 while 루프가 "너무 많이" 루프되었는지 확인하기 위해 간단한 테스트를 하고, 그러한 경우 "해당 공을 들어 올리고" 경기장 중심 쪽으로 약간 이동합니다. 그러면 플레이어에 표시된 물리적 요소를 크게 방해하지 않고 무제한 루프가 끊어집니다.

또한 방어적인 프로그래밍의 가치는 게임 개발 중에 매우 유용했습니다. 예를 들어 도우미 함수 affirmingMessage에는 다음 줄이 있습니다.


alert("Error in affirmingMessage(" + level + ")");

코드 전체에서 유사한 경고가 나타나는 것을 볼 수 있습니다. affirmingMessage 함수의 경우, level이 1과 7 사이에 머물도록 하기 위해 level = level % 7 + 1이 잘못 사용되었습니다. 이 방어 경고가 없다면 코드 오류를 파악하기가 훨씬 힘들 것입니다(올바른 문은 level = ((level-1) % 7) + 1).

브라우저 간 지원

SVG 공 튕기기는 Internet Explorer 9을 사용하여 개발되었습니다. 게임이 완료되고 Internet Explorer 9에서 완전히 작동된 후 이 코드는 Firefox 및 Chrome에서 조정되었습니다. 다행히 해결할 브라우저 간 문제는 다음 몇 개 외에는 별로 없었습니다.

  • 마우스 이벤트 개체
  • getElementById
  • 색 문자열(색 값 직렬화)

마우스 이벤트 개체

프로토타입 Paddle.prototype.mouseMove에서 evt.y가 마우스의 현재 Y 좌표 위치를 결정하는 데 사용되었습니다. evt.yevt.pageY로 변경하면 동일한 정보를 제공하지만 브라우저 간 호환성이 더 우수해집니다. 즉, 다음과 같습니다.


paddle.y.baseVal.value = evt.y - arenaTransformForY - constants.paddleFudgeFactor;

다음으로 변경됨:


paddle.y.baseVal.value = evt.pageY - arenaTransformForY - constants.paddleFudgeFactor;

getElementById

다음 가상 함수를 고려하세요.


function notUsedInGame() {
  paddle.y.baseVal.value = 40;
}

paddle이 이미 존재한다고 가정하면 이 함수는 Internet Explorer 9 및 Chrome 모두에서 있는 그대로 작동합니다. 하지만 Firefox에서는 다음 getElementById 호출이 필요합니다(내용 작성 시점).


function notUsedInGame() {
  var paddle = document.getElementById('paddle');
  paddle.y.baseVal.value = 40;
}

이는 해당 코드 내의 많은 "이례적인" getElementById 호출을 설명해줍니다.

색상 문자열

Paddle.prototype.processCollision에서, 콜드 흰색 공을 흰색이 아닌 핫 색상으로 변경하는 경우를 감지하는 데 다음 코드가 사용되었습니다.


if (ball.style.fill == constants.coldBallColor) 
  ball.style.fill = getRandomColor(); 


초기에 만들어질 때 각 공의 채우기 속성이 constants.coldBallColor("white")로 설정되었지만, 문자열 "white"가 일부 브라우저에서는 #ffffff으로 변환되고 다른 브라우저에서는 rgb(255, 255, 255)로 변환되기 때문에 위의 조건부 테스트가 여러 브라우저에서 실패합니다. 사용된 솔루션은 이 콜드 색상 테스트를 중단하고 대신 공의 hotCount 속성을 사용하는 것이었습니다.


if ( !ball.hotCount )
  ball.style.fill = getRandomColor(); 


Paddle.prototype.processCollision에서 이 테스트는 다음 경우에만 콜드 공에 흰색이 아닌 단색을 지정하는 데 사용됩니다.

  • ball.hotCount가 0(핫 공이 콜드가 됨)
  • ball.hotCountundefined(새로 만든 공이 라켓에 맞아 핫이 됨)

앞에서 설명한 대로 경기장 벽에 너무 많이 튕겨서 핫 공이 콜드가 되는 경우 ball.hotCount는 0이고 공이 처음으로 만들어진 직후(기본적으로 모두 콜드임) ball.hotCountundefined입니다. 이는 JavaScript에서 !undefinedtrue이기 때문에 제대로 작동합니다.

제안되는 연습

이 항목에서 제시된 코드를 완전히 이해할 수 있도록 제안되는 게임 향상 기능은 다음과 같습니다.

  • 단추 누르기 또는 다른 방법을 통해 느리게 이동하는 공의 속도를 높이기 위한 방법을 제공하세요. 예를 들어 연결된 속도 벡터 규모가 특정 임계값 아래인 경우 "s" 키를 누르면 모든 공의 속도가 높아지게 할 수 있습니다.
  • 안쪽 및 바깥쪽 경기장 벽 간의 "잠긴" 튕기기 등 "지루한" 튕기기 패턴이 발생하지 않도록 공에 초기 속도 벡터가 지정되도록 하세요.
  • 사운드 효과 추가는 기본 SVG 애니메이션예제 8을 참조하세요 .

더 복잡한 향상 기능으로는 직사각형 라켓을 타원으로 변경하는 것 등이 있을 수 있습니다. 이렇게 하면 결승점으로 공을 안내하는 시각적으로 보다 정확한 방법이 제공됩니다. 이 경우 공/라켓 충돌 감지를 위한 접근 방식 중 하나로, 공의 원 및 라켓의 타원과 관련된 수식의 체계를 동시에 푸는 것을 들 수 있습니다.

관련 항목

웹 페이지에 SVG를 추가하는 방법
기본 SVG 애니메이션
중급 SVG 애니메이션
SVG 좌표 변환
HTML5 그래픽
Scalable Vector Graphics (SVG)

 

 

표시:
© 2014 Microsoft