WhiteOut Game
이 샘플 코드는 JavaFX 기술로 일종의 Lights Out game을 만드는 방법을 보여준다. 복수개의 화면들, 버튼들, 강조부분, 그리고 변하는 효과 같은 외형을 수동으로 작성하는 것을 보여준다(It shows manual layout, multiple scenes, buttons, highlighting, and transitional effects).
코드 이해하기
코드는 다양한 창 크기에서 작동하게 설계된다. 창의 크기는 현재 모바일 에뮬레이터의 크기인 320 x 240으로 Main안에 정의 됐다. API들이 코드들이 작동하는 화면의 크기를 얻으려 플랫폼에 추가될 때, 창의 크기를 결정하는데 사용된다. 이제 Main의 myWidth 와 myHeight 의 값들을 수정하여 다시 빌드 함하여 다른 크기로 시험해 볼 수 있다.
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의 설정을 변경해 보면 된다.translateX 와 translateY 의 설정값과, 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들의 집결 예제를 포함하고 있다. 번쩍이는 화면이 보여질 때, 다음과 같은 것이 발생한다:
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 생성
다음을 살펴보자:코드 커스터마이징
이 코드에서 주요 커스터마이징을 할 수 있는 부분은:
추가적으로, 몇가지를 더 해볼 수 있다: