2008年10月8日水曜日

JavaFX Air Hockey program


I made a Air Hockey game in JavaFX.
As for the program, named AirHockey.fx, plese refer to the code (modified to conform to JavaFX 1.0).
When you run the program, an air hockey game table with two aqua color mallets are displayed on your screen. Computer's goal (the upper one) and your goal (the lower one) are displayed in burlywood color.
Push the "SET" button to start the game. Then, a white puck appears on your side.
You can manipulate your mallet by the mouse as long as it resides in your area.
A velocity of your mallet is computed from its displacement by applying the scaling transformation. The scaling is modulated by the velocityScaling factor. You can make your mallet's velocity more real by moving the factor closer to 1.0.
You can also regulate the bounce back speed by adjusting the elastic modulus elastic.
In this program, a computer simply move its mallet back and forth. It has no ability to take the offensive. It is your business to make your opponent cool.
Enjoy yourself!

2008年10月7日火曜日

JavaFXで空中ホッケー遊技(Air hockey game)を作る

JavaFXで、空中ホッケー遊技(Air hockey game)を作ってみました。
頭の中が沸騰しそうになったとき、しばらく空っぽにするのにいいですよ。
プログラムを実行すると、右の図のような
空中ホッケーの卓が表示されます。薄茶色で表示されているところが得点圏(goal)で、青緑色で表示されているのが槌(mallet)です。
「SET」ボタンを押すと、図の位置に白い円盤(puck)が表示されます(プログラムを実行した直後や、得点した後は、円盤が表示されません)。
人間様用の槌(下側が「人間様」の領域で、上側が「計算機様」の領域です)をマウスで動かして(人間様の領域内にマウスを持っていくと、その後を槌が追います)、円盤を打ってみてください。
槌を速く動かしすぎると、槌で円盤を引きずるような動作をします。これは、マウスの位置の変化量から槌の速度を計算する際に、スケーリング変換を行っているためです。velocityScalingの値で、変換係数を調節してください。
また、
円盤が卓の壁や槌に衝突してはね返されるとき、弾性係数を考慮して動きを計算しています。elasticの値で、弾性率を変えてみてください。
今回のものでは、計算機様は、ただ槌を往復させるだけで、何の芸も持ち合わせていません。計算機様にどんな芸を持たせるかが、これからの楽しみです。何の制限も持たせなければ、計算機様をいくらでも強くできますが、それでは勝負になりません。強過ぎても弱過ぎてもだめですし、(乱数に因らない)偶然の要素もないと面白くない、その辺りが工夫のしどころです。
最後に、原始コードを載せておきます。なお、前回の投稿で、仕立て節に定義した関数がうまく機能しないと記しましたが、
(部品化を考慮して)仕立て節を別の台本ファイルで定義したことがまずかっただったようです。このプログラムのように、同じ台本ファイル内に仕立て節を定義する限り、特に問題はありませんでした。


/*
* AirHockey.fx
* The air hockey game.
*
* Created and modified:
* V 1.0.0 2008/09/25
*/

package game;

import javafx.animation.*;
import javafx.application.*;
import javafx.input.*;
import javafx.scene.*;
import javafx.scene.effect.*;
import javafx.scene.effect.light.*;
import javafx.scene.geometry.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import java.lang.Math;
import java.lang.System;

/**
* @author terra
*/

var goalCorners: Disc[];
var mouseEvent: MouseEvent;

var tableWidth: Number = 300;
var tableHeight: Number = 400;
var goalWidth: Number = 80;
var goalCornerRadius: Number = 5;
var puckRadius: Number = 10;
var malletRadius: Number = 15;
var hittingAreaDepth: Number = tableHeight / 2 - malletRadius;
var elastic: Number = 0.95;
var velocityScaling = 0.35;
var malletCMaxV = 2; // Maximum velocity of the computer's mallet.

var timingGenerator: Timeline = Timeline {
repeatCount: Timeline.INDEFINITE
keyFrames: KeyFrame {
time: 20ms
action: function(): Void {
malletC.move();
puck.move();
malletH.calcuVelocity();
puck.collide(malletH);
puck.collide(malletC);
for (corner in goalCorners) {
puck.collide(corner);
}
score.judgeGoal();
}
}
};

var puck: Puck = Puck {
// x: goalCornerRadius + tableWidth / 2
// y: goalCornerRadius + 2 * tableHeight/ 3
x: -(puckRadius + 1)
y: goalCornerRadius + tableHeight / 2
radius: puckRadius
vX: 0, vY: 0
}

var malletH: MalletH = MalletH { // Human's mallet.
x: bind mouseEvent.getStageX()
y: bind mouseEvent.getStageY()
radius: malletRadius
color: Color.AQUA
}

var malletC: MalletC = MalletC { // Computer's mallet.
x: goalCornerRadius + tableWidth / 2
y: goalCornerRadius + malletRadius + puckRadius
radius: malletRadius
color: Color.AQUA
vX: malletCMaxV, vY: 0
}

for (i in [0..1]) {
for (j in [0..1]) {
insert Disc {
x: (tableWidth - goalWidth) / 2 + j * goalWidth
y: i * (2 * goalCornerRadius + tableHeight)
radius: goalCornerRadius
color: Color.LIGHTGREY
} into goalCorners;
}
}

var score: Score = Score{}

Frame {
title: "Air hockey"
width: 2 * goalCornerRadius + tableWidth + 56 as Integer
height: 2 * goalCornerRadius + tableHeight + 28 as Integer
visible: true
closeAction: function(): Void {System.exit(0);}
stage: Stage {
width: 2 * goalCornerRadius + tableWidth as Integer
height: 2 * goalCornerRadius + tableHeight as Integer
fill: Color.LIGHTGRAY
content: [
Rectangle { // Jointed goal area.
x: (tableWidth - goalWidth) / 2
y: 0
width: goalWidth
height: 2 * goalCornerRadius + tableHeight +2
fill: Color.BURLYWOOD
},Rectangle { // Table.
x: goalCornerRadius, y: goalCornerRadius
width: tableWidth, height: tableHeight
fill: Color.OLIVE
},
Rectangle { // Human's hittig area.
x: goalCornerRadius + malletRadius
y: goalCornerRadius + tableHeight - hittingAreaDepth
width: tableWidth - 2 * malletRadius
height: hittingAreaDepth - malletRadius
fill: Color.OLIVE
onMouseMoved: function(ev: MouseEvent): Void {
mouseEvent = ev;
}
},
Line { // Center line.
startX: goalCornerRadius
startY: goalCornerRadius + tableHeight / 2
endX: goalCornerRadius + tableWidth -1
endY: goalCornerRadius + tableHeight / 2
stroke: Color.BLUE
},
puck,
malletH,
malletC,
goalCorners,
ScoreIndicator {
x: 2 * goalCornerRadius + tableWidth + 5 as Integer
y: goalCornerRadius + tableHeight / 2 - 24 as Integer
score: bind score.scoreC
},
ScoreIndicator {
x: 2 * goalCornerRadius + tableWidth + 5 as Integer
y: goalCornerRadius + tableHeight / 2 + 5 as Integer
score: bind score.scoreH
},
Rectangle { // "SET" button.
x: 2 * goalCornerRadius + tableWidth + 5 as Integer
y: goalCornerRadius + tableHeight - 30 as Integer
width: 35, height: 30
arcWidth: 8, arcHeight: 8
fill: Color.DARKGREY
effect: Lighting {
light: DistantLight {
azimuth: 225, elevation: 50
}
}
onMousePressed: function(ev: MouseEvent): Void {
puck.x = goalCornerRadius + tableWidth / 2;
puck.y = goalCornerRadius + 2 * tableHeight/ 3;
puck.vX = 0;
puck.vY = 0;
timingGenerator.start();
}
},
Text {
x: 2 * goalCornerRadius + tableWidth + 10 as Integer
y: goalCornerRadius + tableHeight - 20 as Integer
textOrigin: TextOrigin.TOP
fill: Color.YELLOW
font: Font {
size: 14
name: "Monospaced", style: FontStyle.BOLD
}
effect: DropShadow {
offsetX: 3, offsetY: 3, radius: 2
color: Color.BLACK
}
content: "SET"
}
]
}
}


class Disc extends CustomNode {
attribute x: Number;
attribute y: Number;
attribute radius: Number;
attribute color: Color = Color.WHITE;
attribute vX: Number;
attribute vY: Number;
attribute lastX: Number = x;
attribute lastY: Number = y;

function create(): Node {
return Circle {
centerX: bind x, centerY: bind y
radius: bind radius
fill: bind color
};
}
}

class Puck extends Disc {
function move(): Void {
x += vX;
y += vY;

if (x + radius > goalCornerRadius + tableWidth) {
x = goalCornerRadius + tableWidth - radius;
vX *= -1.0 * elastic;
} else if (x - radius < goalCornerRadius) {
x = goalCornerRadius + radius;
vX *= -1.0 * elastic;
}

if (x >= 0) {
if (y > 2 * goalCornerRadius + tableHeight) {
// It crossed the Human's goal line.
x = -(radius + 1);
y = 2 * goalCornerRadius + tableHeight + radius;
vX = 0;
vY = 0;
} else if (y + radius > tableHeight + goalCornerRadius) {
if ((x < (tableWidth - goalWidth) / 2)
or (x > (tableWidth + goalWidth) / 2)) {
y = tableHeight + goalCornerRadius - radius;
vY *= -1.0 * elastic;
}
} else if (y < 0) {
// It crossed the computer's goal line.
x = -(radius + 1);
y = -radius;
vX = 0;
vY = 0;
} else if (y - radius < goalCornerRadius) {
if ((x < (tableWidth - goalWidth) / 2)
or (x > (tableWidth + goalWidth) / 2)) {
y = goalCornerRadius + radius;
vY *= -1.0 * elastic;
}
}
}
}

function collide(disc: Disc): Void {
var distX: Number = disc.x - x;
var distY: Number = disc.y - y;
var minDist: Number = disc.radius + radius;
var dist2: Number = distX * distX + distY * distY;
var minDist2: Number = minDist * minDist;

if (dist2 < minDist2) {
var colAngle: Number = Math.atan2(distY, distX);
var sinColAngle: Number = Math.sin(colAngle);
var cosColAngle: Number = Math.cos(colAngle);

var expres1: Number = disc.vX * cosColAngle + disc.vY * sinColAngle;
var expres2: Number = vX * sinColAngle - vY * cosColAngle;
var expres3: Number = vX * cosColAngle + vY * sinColAngle;

x = disc.x - minDist * cosColAngle;
y = disc.y - minDist * sinColAngle;

vX = (1+ elastic) * expres1 * cosColAngle + expres2 * sinColAngle
- elastic * expres3 * cosColAngle;
vY = (1+ elastic) * expres1 * sinColAngle - expres2 * cosColAngle
- elastic * expres3 * sinColAngle;
}
}
}

class MalletH extends Disc {
function calcuVelocity(): Void {
vX = velocityScaling * (x - lastX);
vY = velocityScaling * (y - lastY);
lastX = x;
lastY = y;
}
}

class MalletC extends Disc {
function move(): Void {
x += vX;

if (x + radius > 2 * goalCornerRadius + tableWidth / 2
+ goalWidth / 2) {
x = 2 * goalCornerRadius + tableWidth / 2 + goalWidth / 2
- radius;
vX *= -1.0;
} else if (x - radius < tableWidth / 2 - goalWidth / 2) {
x = tableWidth / 2 - goalWidth / 2 + radius;
vX *= -1.0;
}
}
}

class Score {
attribute scoreC: Integer = 0; // Computer's score.
attribute scoreH: Integer = 0; // Human's score.

function judgeGoal(): Void {
if (puck.x < 0) {
if (puck.y > 2 * goalCornerRadius + tableHeight) {
scoreC += 1;
} else if (puck.y < 0) {
scoreH += 1;
}
puck.y = goalCornerRadius + tableHeight / 2;
timingGenerator.stop();
}
}
}

class ScoreIndicator extends CustomNode {
attribute score: Integer = 0;
attribute x: Integer = 0;
attribute y: Integer = 0;
attribute width: Integer = 21;
attribute height: Integer = 18;
attribute fillColor: Color = Color.WHITE;
attribute borderColor: Color = Color.YELLOWGREEN;
attribute scoreTextColor: Color = Color.BLACK;

function create(): Node {
Group {
content: [
Rectangle {
x: x, y: y
width: width, height: height
arcWidth: 6, arcHeight: 6
stroke: borderColor, fill: fillColor
},
Text {
x: x + 4 * height / 18 as Integer
y: y + 5 * height / 18 as Integer
textOrigin: TextOrigin.TOP
fill: scoreTextColor
font: Font {
size: 14 * height / 18 as Integer
name: "Monospaced", style: FontStyle.BOLD
}
content: bind "{%2d score}"
}
]
}
}
}