WhiteOut Game

By Jim Holmlund, Nov 4, 2008

이 샘플 코드는 JavaFX 기술로 일종의 Lights Out game을 만드는 방법을 보여준다. 복수개의 화면들, 버튼들, 강조부분, 그리고 변하는 효과 같은 외형을 수동으로 작성하는 것을 보여준다(It shows manual layout, multiple scenes, buttons, highlighting, and transitional effects).

코드 이해하기

코드는 다양한 창 크기에서 작동하게 설계된다. 창의 크기는 현재 모바일 에뮬레이터의 크기인 320 x 240으로 Main안에 정의 됐다. API들이 코드들이 작동하는 화면의 크기를 얻으려 플랫폼에 추가될 때, 창의 크기를 결정하는데 사용된다. 이제 MainmyWidthmyHeight 의 값들을 수정하여 다시 빌드 함하여 다른 크기로 시험해 볼 수 있다.

Env class는 그림 1처럼, 창의 크기에서 여러가지 GUI 가젯의 크기와 위치를 계산하는데 사용한다.

소스 코드
    public def smallFont = 
        if (screenWidth < 320) 
            Font {size: 15}
        else
            Font {size: 20
    }
    def resetSize = Text {
        content: "Reset"
        font: smallFont
    };

    // Leave a bit of space around "Reset"
    public def blueButtonHeight = resetSize.layoutBounds.height + 8;
    public def blueButtonWidth = resetSize.layoutBounds.width + 10;

    public def nButtons = 5;

    // This is the gap we will leave between game buttons, and we want at least this
    // much margin at the top / bottom of the screen.  That makes 5 buttons and 6 gaps.
    def gameButtonGapPercent = .1;

  

그림 1: GUI 가젯들의 크기와 위치 계산

또한, Env.fx에서 게임 버튼들(5x5 배열안의 버튼들)의 크기 연산에 주목하라. 이 연산은 창의 넓이에 따른 버튼의 원하는 크기와 카운터 이동과 리셋 그리고 종료 버튼들의 적당한 위치에 필요한 여백를 계산한다(This computation computes the desired size of a button based on the width of the window, and the space needed to the right of the buttons for the move counter and the reset and quit buttons). 창에서 버튼 배열이 적당한 위치에서 더 높게 있다면, 연산으로 그 크기를 맞춘다.

layout 형태로 만든 class들은 Env의 값들을 사용한다. 예들들면, 그림 2처럼 BlueButton class는BlueButton의 크기를 사용한다.

소스 코드
    public class BlueButton extends Group {
        public-init var env: Env;
        public-init var text: String;
        // We will add a 2 pixel shadow.
        public-init var width = env.blueButtonWidth - 2;
        public-init var height = env.blueButtonHeight - 2;
 

그림 2: BlueButton Class는 BlueButton을 사용함

GUI 가젯의 외형이 어떻게 변하는 보려면 Env의 설정을 변경해 보면 된다.translateXtranslateY 의 설정값과, layout 생성시 layoutBounds 의 사용에 대해 확인해 보라. 예제는, Canvas.fx 에서 다음의 코드를 보라.
소스 코드
    def moves = Text {
        content: "Moves"
        translateX: env.buttonX
        textOrigin: TextOrigin.TOP
        fill: Color.WHITE
        font: env.smallFont
    };

    def moveCount: Text =  Text {
        content: bind model.moveCount.intValue().toString()
        translateX: bind env.buttonX + (moves.layoutBounds.width  - moveCount.layoutBounds.width) / 2,
        textOrigin: TextOrigin.TOP
        textAlignment: TextAlignment.CENTER
        fill: Color.WHITE
        font: env.mediumFont
    };
 

그림 3: "Moves"의 x 좌표와 이동 횟수 설정

그림 3에서의 코드는 "Moves"란 단어의 x 좌표와 바로 아래에 보이는 이동 횟수를 설정한다. 횟수에 대해서, x 좌표는 count filed의 넓이가 표함된 표현하기 위한 "범위"라는 것에 주의하라(Note that for the count, the x coordinate is "bound" to an expression that involves the width of the count field). 이 넓이가 한자리에서 두자리로 바뀜에 따라, x 좌표가 변하고, "Moves"란 단어 아래에 자리잡은 횟수의 위치를 유지시킨다(As this width changes from one digit to two, the x coordinate changes, which keeps the count centered under the word "Moves"). 나중에 Canvas.fx에서 "Moves"란 단어와 횟수의 y 좌표를 정의하는 다음 코드를 찾자.
소스 코드

    moves.translateY = resetButton.translateY + resetButton.layoutBounds.maxY + 
                 ((quitButton.translateY + quitButton.layoutBounds.minY) -
                  (resetButton.translateY + resetButton.layoutBounds.maxY) - 
                  (moves.layoutBounds.height + moveCount.layoutBounds.height + 7)) / 2;

    moveCount.translateY = moves.translateY + moves.layoutBounds.maxY + 7

그림 4: "Moves"와 횟수의 y 좌표 설정

그림 4의 코드에의 가운데에는 layoutBounds 을 포함하여 작동하는 translateY 값들로 설정된 리셋 버튼과 종료 버튼들 사이에 "Moves"와 횟수를 포함한 두 줄이 있다.(The code in Figure 4 centers the two lines containing "Moves" and the count between the Reset and Quit buttons by setting their translateYs to functions that involve layoutBounds. )

게임에서 버튼들은 마우스가 버튼에 들어오거나 나감에 따라 그림자 효과와 테두리가 희미해지거나 진해진다.(The buttons in the game have drop shadows and borders that fade in or fade out as the mouse enters or leaves the button.) 이 효과들은, 아래와 오른쪽인 두개의 픽셀로 계산된 것과, 어두운 색 값을 가진 두개의 사각형을 생성함으로 구현된다. (These effects are achieved by creating two rectangles, one offset down and right by two pixels, and with a dark color.) 이것이 그림자 효과이다.(This is the drop shadow.) 다른 사각형은 버튼의 속성이다. 사각형에 그려진 선, "stroke"를 포함한다. 이 선의 불투명도는 마우스가 들어오거나 나감에 따라 바뀌는 strokeAlpha 란 변수에 "묶여있음"에 주의하라. 다음의 BlueButton.fx의 코드를 보라.
소스 코드
    content = [
        // This creates a shadow on the bottom and right of the button
        Rectangle {
            fill: Color.rgb(0x30, 0x30, 0x30)
            translateX: 2
            translateY: 2
            width: width
            height: height
            arcWidth: 6
            arcHeight: 6
        }
        Rectangle {
            fill: env.blueGrad
            width: width
            height: height
            arcWidth: 6
            arcHeight: 6
            stroke: bind Color{red: 1, green: 1, blue: 1, opacity: strokeAlpha}
             onMouseEntered: function(e) {
                fade.rate = 10.0;
                fade.play();
            }
             onMouseExited: function(e) {
                fade.rate = -10.0;
                fade.play();
            }
        }

그림 5: 버튼들에게 그림자 효과와 테두리 생성하기

그림 5에서 fade.play()가 호출됨을 주의하라. 'fade'는 Timeline 변수이다:

소스 코드
    def fade = Timeline {
        keyFrames: [
            at(0s)   { strokeAlpha => 0.3 tween Interpolator.LINEAR }
            at(0.5s) { strokeAlpha => 1.0 tween Interpolator.LINEAR }
        ]
    };

fade.play() 구문으로 이 timeline이 활성화 될 때, strokeAlpha 변수값은 0.3에서 1.0으로 0.5초를 경과하여 동시에 변한다. 이 변동은 사각형 stroke를 불투명하게 바꾼다. 비슷하게, 마우스가 빠져나가면, timeline은 반대로 사각형 stroke를 거의 투명하게 만든다.

Splash class는 번쩍이는 화면을 보여주고 Timeline들의 집결 예제를 포함하고 있다. 번쩍이는 화면이 보여질 때, 다음과 같은 것이 발생한다:

  • 번쩍이는 화면은 투명한 상태에서 불투명하게 되여 보여질 때 나타난다.
  • "White"란 단어는 화면 왼쪽에서 미끄러져 들어오고 "Out"이란 단어는 화면 오른쪽에서 미끄러져 들어온다.
  • Start 버튼은 제 위치로 올라온다.
  • 이 효과가 그림 6에서의Timeline에 의해 생성된다.
    소스 코드
     
        textWhite.x = -(textWhite.layoutBounds.width + 1);
        textWhite.visible = true;
    
        textOut.x = env.screenWidth + 1;
        textOut.visible = true;
        opacity = 0.0;
    
        def opa = Timeline {
            keyFrames: [
               at (.5s) {opacity => 1.0 tween Interpolator.LINEAR}
               KeyFrame {
                   time: .4s
                   timelines: Timeline {
                       keyFrames: [
                           at (.4s) {textWhite.x => finalWhiteX tween Interpolator.LINEAR}
                           at (.4s) {textOut.x => finalOutX tween Interpolator.LINEAR}
                       ]
                   }
               }
               KeyFrame {
                   time: 1s
                   timelines: Timeline {
                       rate: 1.0
                       keyFrames: [
                          at (.5s) {buttonY => finalButtonY tween spring}
                       ]
                   }
               }
            ]
        }
        opa.play();
    

    그림 6: Timeline 생성

    다음을 살펴보자:
  • 0에서 0.5초가 되면서, 불투명도는 자신의 고유값을 0.0에서 1.0으로 동시에 변동하여, 내용을 보이게 한다.
  • 0.4초전에는, "White"와 "Out"이란 단어는 화면의 왼쪽과 오른쪽 바깥에 있다. 0.4에서 0.8초로 변하면서, 이 단어들은 각각의 최종 위치로 미끄러져간다.
  • 1초전에는, Start 버튼은 화면 아래에 있다. 1에서 1.5초로 변하면서, 이 버튼은 솟아나오는 효과를 수행하는 CustomInterpolator를 사용하여 최종 위치로 이동한다. 변수 buttonYstartButtontranslateY 필드에 묶여 있어서, buttonY 가 변하면, startButton 이 움직인다.
  • 코드 커스터마이징

    이 코드에서 주요 커스터마이징을 할 수 있는 부분은:

  • Main.fx 에서 창의 크기를 해 볼수 있다. 현재의 외관에서 약 200 x 200 으로 낮춰도 잘 작동한다. 그 크기 이하는, 한계크기 제한이 생길것이다.
  • 외형에 대한 상수와 Env.fx 에서의 연산을 변경 해 볼 수 있다. 예를 들면, nButtons을 30으로 바꿔보라.

  • 추가적으로, 몇가지를 더 해볼 수 있다:
  • Model.fx 에서 게임 버튼들은 사각형으로 정의되어 있다. 그것들을 원형으로 변경해보라.
  • Env.fx 에서 버튼들은 blue gradient로 정의되어 있다. 버튼들을 다른 색이나 다른 gradient로 변경해보라.
  • Stage는 크기변경이 되지 않음을 유의하라. 크기를 변경하게 만들고 싶다면, 그것을 더 크게나 작게 드래그 하면 되지만 내용은 확장되거나 축소되지 않는다. Stage의 크기나 넓이에 관련된 주요 변수들을 묶어서 크기가 변경되면, 외관도 재구성되게 할 수 있을까? (Could you bind key variables to the size and width of the Stage so that when it is resized, the layout is redone?) 쉬운 대안은 새로운 더 크거나 작은 창 크기를 생성하는 두개의 버튼을 추가하여 , 이전 창에서 제거하고, 현재 이동 위치에서 새 창으로 게임을 시작한다(An easier alternative would be to add two buttons that would just create a new larger or smaller window size, remove the old window, and start the game in the new window at the current move position).
  • 수평과 수직으로, 실린더처럼 작동하는 버튼들의 배열을 만들어 게임을 수정해 보라. 예를 들면, 버튼[0,3]의 "이웃"은 버튼[0,2]와 [1,3]이다. 이것은 [4,3]의 추가와 [0,0]이 이웃함으로 변경된다.(For example, the "neighbors" of button[0,3] are [0,2] and [1,3]. This change would add [4,3] and [0,0] as neighbors.)
  • white 버튼들을 모두 꺼버리는 일련의 클릭으로 게임을 해결해 볼 수 있는 해결 버튼을 추가해 보라( Add a Solve It button that would cause the game to simulate a series of clicks that would turn off all the white buttons).
  • Note that the initial configuration of the buttons is achieved by simulating a few clicks. This means that the puzzle can be solved by repeating those clicks. Instead of simulating a few clicks, try just randomly setting a few buttons to be white. This change can result in unsolvable configurations.
  • Note that the clicks simulated to set up the initial configuration are restricted to a 4x4 square. If you increase the number of buttons, say to a 10x10 square, this initial configuration is a bit confining. Try increasing the 4x4 restriction to the full 10x10. Note that now the simulated clicks tend to be far apart so that they don't interact, leaving a trivially solvable puzzle. Find a new algorithm for setting the initial configuration that solves this problem.