comparison game/Engine.java @ 162:e8eeac403e5f

Backed out changeset fb33d3796942
author Matti Hamalainen <ccr@tnsp.org>
date Thu, 01 Dec 2016 14:33:25 +0200
parents src/Engine.java@fb33d3796942
children 2b2fa62cfea5
comparison
equal deleted inserted replaced
161:fb33d3796942 162:e8eeac403e5f
1 /*
2 * Ristipolku Game Engine
3 * (C) Copyright 2011 Matti 'ccr' Hämäläinen <ccr@tnsp.org>
4 */
5 package game;
6
7 import java.awt.*;
8 import java.awt.geom.*;
9 import java.awt.event.*;
10 import java.awt.image.*;
11 import java.awt.event.*;
12 import java.awt.font.*;
13 import javax.imageio.*;
14 import javax.swing.*;
15 import java.util.*;
16 import java.util.concurrent.locks.Lock;
17 import java.util.concurrent.locks.ReentrantReadWriteLock;
18 import java.io.*;
19 import game.*;
20 import javax.sound.sampled.*;
21
22
23 class AboutBox extends IDMWidget
24 {
25 BufferedImage aboutImg;
26 boolean aboutSecret;
27
28 IDMPoint currPos, currOffs;
29 Paint textPaint;
30 Font textFont;
31 FontMetrics textMetrics;
32
33
34 public AboutBox()
35 {
36 try {
37 ResourceLoader res = new ResourceLoader("graphics/girl.jpg");
38 aboutImg = ImageIO.read(res.getStream());
39 }
40 catch (IOException e)
41 {
42 }
43
44 aboutSecret = false;
45
46 setPos(150f, 150f);
47 setSize(675f, 420f);
48 }
49
50 public void setTextFont(Font font, FontMetrics metrics)
51 {
52 textFont = font;
53 textMetrics = metrics;
54 }
55
56 public void setTextPaint(Paint paint)
57 {
58 textPaint = paint;
59 }
60
61 public void setCurrPos(IDMPoint npos)
62 {
63 currPos = npos;
64 currOffs = new IDMPoint(0, 0);
65 }
66
67 public void setCurrPos(float x, float y)
68 {
69 setCurrPos(new IDMPoint(x, y));
70 }
71
72 public void drawString(Graphics2D g, String text)
73 {
74 Paint savePaint = g.getPaint();
75 g.setPaint(textPaint);
76 g.setFont(textFont);
77
78 int i = 0;
79 while (i < text.length())
80 {
81 int p = text.indexOf("\n", i);
82 boolean linefeed;
83 String str;
84 if (p >= i)
85 {
86 str = text.substring(i, p);
87 i = p + 1;
88 linefeed = true;
89 }
90 else
91 {
92 str = text.substring(i);
93 i += str.length();
94 linefeed = false;
95 }
96
97 g.drawString(str, currPos.x + currOffs.x, currPos.y + currOffs.y);
98
99 if (linefeed)
100 {
101 currOffs.x = 0;
102 currOffs.y += textMetrics.getHeight();
103 }
104 else
105 {
106 currOffs.x += textMetrics.stringWidth(str);
107 }
108 }
109
110 g.setPaint(savePaint);
111 }
112
113 public void paint(Graphics2D g)
114 {
115 int x = getScaledX(), y = getScaledY(),
116 w = getScaledWidth(), h = getScaledHeight();
117
118 g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.7f));
119 g.fill(new RoundRectangle2D.Float(x, y, w, h, 10, 10));
120
121 setTextFont(G.fonts[3], G.metrics[3]);
122 setTextPaint(Color.white);
123
124 setCurrPos(x + 20, y + 30);
125 drawString(g, "RistiPolku (CrossPaths) v"+ G.version +"\n");
126
127 setTextFont(G.fonts[1], G.metrics[1]);
128 if (aboutSecret)
129 {
130 g.drawImage(aboutImg, x + 20, y + 55,
131 aboutImg.getWidth(), aboutImg.getHeight(), null);
132
133 setCurrPos(x + 225, y + 75);
134 drawString(g,
135 "Dedicated to my\n" +
136 "favorite woman\n" +
137 "in the world.");
138
139 setCurrPos(x + 370, y + 175);
140 drawString(g, "- Matti");
141 }
142 else
143 {
144 setTextPaint(Color.yellow);
145 drawString(g, "(c) Copyright 2011 Tecnic Software productions (TNSP)\n");
146
147 setTextPaint(Color.white);
148 drawString(g, "Programming, graphics and design by " +
149 "Matti 'ccr' Hämäläinen.\n" +
150 "Audio from archive.org, used non-commercially.\n \n");
151
152 setTextPaint(Color.red);
153 drawString(g, "Controls:\n");
154
155 IDMPoint old = currOffs.copy();
156
157 setTextPaint(Color.white);
158 drawString(g,
159 "Arrow keys / mouse wheel\n"+
160 "Enter / mouse click\n"+
161 "Space bar\n");
162
163 currPos.x += 330;
164 currOffs.y = old.y;
165 drawString(g,
166 "- Rotate piece\n" +
167 "- Place/lock piece\n" +
168 "- Swap piece\n");
169
170 currPos.x -= 330;
171 setTextPaint(Color.green);
172 drawString(g,
173 "\nObjective: Create a path long as possible by rotating\n"+
174 "and placing pieces. More points will be awarded for\n"+
175 "advancing the path by several segments per turn."
176 );
177 }
178 }
179
180 public boolean keyPressed(KeyEvent e)
181 {
182 if (e.getKeyCode() == KeyEvent.VK_L)
183 {
184 aboutSecret = true;
185 }
186 else
187 {
188 clicked();
189 aboutSecret = false;
190 }
191 return true;
192 }
193
194 public void clicked()
195 {
196 parent.remove(this);
197 }
198 }
199
200
201 class GameBoard extends IDMWidget
202 {
203 static final int boardSize = 9;
204 static final int boardMiddle = 4;
205 Piece[][] board;
206 float pscale, ptime;
207
208 public boolean flagGameOver, flagBoardIsDirty;
209 int gameScore;
210
211 Piece currPiece, nextPiece;
212 int currX, currY, currPoint;
213
214 Sound sndPlaced;
215
216 private final ReentrantReadWriteLock pointLock = new ReentrantReadWriteLock();
217 private ArrayList<AnimatedPointElement> pointElems;
218
219 public GameBoard(IDMPoint pos, float ps)
220 {
221 super(pos);
222 pscale = ps;
223
224 // sndPlaced = G.smgr.getSound("sounds/placed.wav");
225
226 pointElems = new ArrayList<AnimatedPointElement>();
227
228 startNewGame();
229 }
230
231 public void startNewGame()
232 {
233 board = new Piece[boardSize][boardSize];
234 board[boardMiddle][boardMiddle] = new Piece(PieceType.START);
235
236 currX = boardMiddle;
237 currY = boardMiddle;
238 currPoint = 0;
239
240 currPiece = null;
241 nextPiece = new Piece(PieceType.ACTIVE);
242
243 flagGameOver = false;
244 flagBoardIsDirty = true;
245 pieceFinishTurn();
246 gameScore = 0;
247 }
248
249 public void paintBackPlate(Graphics2D g)
250 {
251 g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.2f));
252 g.setStroke(new BasicStroke(5.0f));
253 g.draw(new RoundRectangle2D.Float(getScaledX(), getScaledY(),
254 boardSize * pscale, boardSize * pscale,
255 pscale / 5, pscale / 5));
256 }
257
258 public void paintBoard(Graphics2D g, boolean drawCurrent)
259 {
260 for (int y = 0; y < boardSize; y++)
261 for (int x = 0; x < boardSize; x++)
262 if (board[x][y] != null)
263 {
264 if ((drawCurrent && board[x][y] == currPiece) ||
265 (!drawCurrent && board[x][y] != currPiece))
266 {
267 board[x][y].paint(g,
268 getScaledX() + (x * pscale),
269 getScaledY() + (y * pscale),
270 pscale - pscale / 10);
271 }
272 }
273 }
274
275 public void paint(Graphics2D g)
276 {
277 paintBoard(g, true);
278
279 Lock read = pointLock.readLock();
280 read.lock();
281 try
282 {
283 for (AnimatedPointElement elem : pointElems)
284 {
285 elem.paint(g);
286 }
287 }
288 finally
289 {
290 read.unlock();
291 }
292
293
294 if (!flagGameOver)
295 {
296 if (nextPiece != null)
297 {
298 // Draw next piece
299 AffineTransform save = g.getTransform();
300 nextPiece.paint(g, G.screenDim.width * 0.85f - 90.0f/2.0f, G.screenDim.height * 0.43f, 90.0f);
301 g.setTransform(save);
302 }
303 }
304 else
305 {
306 // Game over text
307 String text = "Game Over!";
308 int textWidth = G.metrics[2].stringWidth(text);
309 g.setFont(G.fonts[2]);
310
311 g.setPaint(new Color(0.0f, 0.0f, 0.0f, 0.5f));
312 g.drawString(text, (G.screenDim.width - textWidth) / 2 + 5, G.screenDim.height / 2 + 5);
313
314 double f = Math.sin(ptime * 0.1) * 4.0;
315 g.setPaint(Color.white);
316 g.drawString(text, (G.screenDim.width - textWidth) / 2 + (float) f, G.screenDim.height / 2 + (float) f);
317 }
318
319 // Score
320 String text = ""+ String.format("%05d", gameScore);
321 int textWidth = G.metrics[2].stringWidth(text);
322 g.setFont(G.fonts[2]);
323 g.setPaint(Color.white);
324 g.drawString(text, (G.screenDim.width * 0.85f) - textWidth / 2, G.screenDim.height * 0.3f);
325 }
326
327 public boolean contains(float x, float y)
328 {
329 return (x >= getScaledX() &&
330 y >= getScaledY() &&
331 x < getScaledX() + boardSize * pscale &&
332 y < getScaledY() + boardSize * pscale);
333 }
334
335 public boolean isBoardDirty()
336 {
337 if (flagBoardIsDirty)
338 {
339 flagBoardIsDirty = false;
340 return true;
341 }
342 else
343 return false;
344 }
345
346 public void animate(float time)
347 {
348 if (nextPiece != null)
349 {
350 nextPiece.animate(time);
351 }
352
353 ptime = time;
354 for (int y = 0; y < boardSize; y++)
355 for (int x = 0; x < boardSize; x++)
356 if (board[x][y] != null)
357 {
358 board[x][y].animate(time);
359 if (board[x][y] != currPiece && board[x][y].active)
360 flagBoardIsDirty = true;
361 }
362
363 Lock write = pointLock.writeLock();
364 write.lock();
365 try
366 {
367 ArrayList<AnimatedPointElement> tmp = new ArrayList<AnimatedPointElement>();
368
369 for (AnimatedPointElement elem : pointElems)
370 {
371 elem.animate(time);
372 if (elem.active)
373 tmp.add(elem);
374 }
375
376 pointElems = tmp;
377
378 if (time % 32 == 1)
379 {
380 Random rnd = new Random();
381 pointElems.add(new AnimatedPointElement(
382 new IDMPoint(10 + rnd.nextInt(400), 10 + rnd.nextInt(100)), "."));
383 }
384 }
385 finally
386 {
387 write.unlock();
388 }
389 }
390
391 public void pieceRotate(Piece.RotateDir dir)
392 {
393 if (currPiece != null && !flagGameOver)
394 {
395 currPiece.rotate(dir);
396 }
397 }
398
399 // Change coordinates based on the "outgoing"
400 // piece connection point.
401 private void pieceMoveTo(int point)
402 {
403 switch (point)
404 {
405 case 0: currY--; break;
406 case 1: currY--; break;
407
408 case 2: currX++; break;
409 case 3: currX++; break;
410
411 case 4: currY++; break;
412 case 5: currY++; break;
413
414 case 6: currX--; break;
415 case 7: currX--; break;
416 }
417 }
418
419 public void pieceCreateNew()
420 {
421 currPiece = nextPiece;
422 currPiece.changed();
423 nextPiece = new Piece(PieceType.ACTIVE);
424 flagBoardIsDirty = true;
425 }
426
427 public void pieceSwapCurrent()
428 {
429 if (!flagGameOver)
430 {
431 Piece tmp = currPiece;
432 currPiece = nextPiece;
433 nextPiece = tmp;
434 board[currX][currY] = currPiece;
435 currPiece.changed();
436 nextPiece.changed();
437 flagBoardIsDirty = true;
438 }
439 }
440
441 // Check one piece, set connections, find the new placement
442 // based on piece rotations etc.
443 private boolean pieceCheck(Piece piece)
444 {
445 if (piece == null)
446 {
447 // Create new piece
448 pieceCreateNew();
449 board[currX][currY] = currPiece;
450 return true;
451 }
452 else
453 if (piece.getType() == PieceType.START)
454 {
455 if (currPiece != null)
456 {
457 // Hit center starting piece, game over
458 flagGameOver = true;
459 return true;
460 }
461 else
462 {
463 // Start piece as first piece means game is starting
464 pieceMoveTo(currPoint);
465 pieceCreateNew();
466 board[currX][currY] = currPiece;
467 return true;
468 }
469 }
470
471 // Mark the current piece as locked
472 piece.setType(PieceType.LOCKED);
473
474 // Solve connection (with rotations) through the piece
475 currPoint = piece.getRotatedPoint(piece.getMatchingPoint(currPoint));
476
477 // Mark connection as active
478 piece.setConnectionState(currPoint, true);
479
480 // Solve exit point (with rotations)
481 currPoint = piece.getAntiRotatedPoint(piece.getConnection(currPoint));
482
483 // Move to next position accordingly
484 pieceMoveTo(currPoint);
485 return false;
486 }
487
488 // Finish one move/turn of the game, resolve path and find placement
489 // of the next piece, or set "game over" state if required.
490 public void pieceFinishTurn()
491 {
492 boolean finished = false;
493 int connections = 0;
494
495 if (currPiece != null)
496 {
497 G.smgr.play(sndPlaced);
498 }
499
500 while (!finished)
501 {
502 if (currX >= 0 && currX < boardSize && currY >= 0 && currY < boardSize)
503 {
504 int oldX = currX, oldY = currY;
505 connections++;
506 finished = pieceCheck(board[currX][currY]);
507
508 if (!finished)
509 {
510 Lock write = pointLock.writeLock();
511 write.lock();
512 try
513 {
514 pointElems.add(new AnimatedPointElement(
515 new IDMPoint(
516 getScaledX() + ((oldX + 0.5f) * pscale),
517 getScaledY() + ((oldY + 0.5f) * pscale)),
518 "" + connections));
519 }
520 finally
521 {
522 write.unlock();
523 }
524 }
525
526 }
527 else
528 {
529 // Outside of the board, game over
530 finished = true;
531 flagGameOver = true;
532 }
533 }
534
535 // Compute and add score
536 gameScore += connections * connections;
537
538 // If game over, clear the game
539 if (flagGameOver)
540 {
541 currPiece = null;
542 }
543
544 flagBoardIsDirty = true;
545 }
546
547 public boolean mouseWheelMoved(MouseWheelEvent e)
548 {
549 int notches = e.getWheelRotation();
550
551 if (notches < 0)
552 pieceRotate(Piece.RotateDir.LEFT);
553 else
554 pieceRotate(Piece.RotateDir.RIGHT);
555
556 return true;
557 }
558
559 public void clicked()
560 {
561 if (!flagGameOver)
562 pieceFinishTurn();
563 }
564
565 public boolean keyPressed(KeyEvent e)
566 {
567 if (flagGameOver)
568 return false;
569
570 switch (e.getKeyCode())
571 {
572 case KeyEvent.VK_LEFT:
573 case KeyEvent.VK_UP:
574 pieceRotate(Piece.RotateDir.LEFT);
575 return true;
576
577 case KeyEvent.VK_RIGHT:
578 case KeyEvent.VK_DOWN:
579 pieceRotate(Piece.RotateDir.RIGHT);
580 return true;
581
582 case KeyEvent.VK_ENTER:
583 pieceFinishTurn();
584 return true;
585 }
586 return false;
587 }
588 }
589
590
591 public class Engine extends JPanel
592 implements Runnable, KeyListener,
593 MouseListener, MouseWheelListener
594 {
595 long startTime;
596 float gameUpdates, gameFrames;
597
598 Thread animThread;
599 boolean animEnable = false;
600
601 GameBoard lauta = null;
602 InputStream musa;
603 IDMContainer widgets;
604 AboutBox aboutBox;
605
606 public void dbg(String msg)
607 {
608 System.out.print("Engine: " + msg);
609 }
610
611 public Engine()
612 {
613 // Initialize globals
614 System.out.print("Engine() constructor\n");
615
616 // Sound system
617 G.smgr = new SoundManager(new AudioFormat(22050, 16, 1, true, false), 1);
618
619 // Load resources
620 try
621 {
622 ResourceLoader res = new ResourceLoader("graphics/board.jpg");
623 G.lautaBG = ImageIO.read(res.getStream());
624
625 try {
626 res = new ResourceLoader("graphics/font.ttf");
627
628 G.fonts = new Font[G.numFonts];
629 G.fonts[0] = Font.createFont(Font.TRUETYPE_FONT, res.getStream());
630 G.fonts[1] = G.fonts[0].deriveFont(24f);
631 G.fonts[2] = G.fonts[0].deriveFont(64f);
632 G.fonts[3] = G.fonts[0].deriveFont(32f);
633 }
634 catch (FontFormatException e)
635 {
636 dbg("Could not initialize fonts.\n");
637 }
638
639 res = new ResourceLoader("sounds/gamemusic.wav");
640 musa = res.getStream();
641 }
642 catch (IOException e)
643 {
644 JOptionPane.showMessageDialog(null,
645 e.getMessage(),
646 "Initialization error",
647 JOptionPane.ERROR_MESSAGE);
648
649 dbg(e.getMessage());
650 }
651
652 // Create IDM GUI widgets
653 widgets = new IDMContainer();
654
655 lauta = new GameBoard(new IDMPoint(95, 130), 63);
656 widgets.add(lauta);
657
658 widgets.add(new BtnSwapPiece(767f, 450f));
659 widgets.add(new BtnAbout (767f, 550f));
660 widgets.add(new BtnNewGame (767f, 630f));
661
662 aboutBox = new AboutBox();
663
664 // Game
665 startNewGame();
666
667 // Initialize event listeners
668 addKeyListener(this);
669 addMouseListener(this);
670 addMouseWheelListener(this);
671
672 // Start playing background music
673 G.smgr.play(musa);
674
675 // Get initial focus
676 if (!hasFocus())
677 {
678 dbg("Requesting focus.\n");
679 requestFocus();
680 }
681
682 gameUpdates = 0;
683 }
684
685 public void startNewGame()
686 {
687 gameFrames = 0;
688 startTime = new Date().getTime();
689 lauta.startNewGame();
690 }
691
692 public void paintComponent(Graphics g)
693 {
694 Graphics2D g2 = (Graphics2D) g;
695 boolean scaleChanged = false,
696 updateBoard = lauta.isBoardDirty();
697
698 // Rescale if parent component size has changed
699 Dimension dim = getSize();
700 if (G.screenDim == null || !dim.equals(G.screenDim))
701 {
702 float dw = dim.width / 1024.0f,
703 dh = dim.height / 768.0f;
704
705 // Rescale IDM GUI widgets
706 widgets.setScale(dw, dh);
707 G.screenDim = dim;
708
709 // Rescale background image
710 // Rescale fonts
711 G.fonts[1] = G.fonts[0].deriveFont(24f * dw);
712 G.fonts[2] = G.fonts[0].deriveFont(64f * dw);
713 G.fonts[3] = G.fonts[0].deriveFont(32f * dw);
714
715 dbg("Scale changed.\n");
716 scaleChanged = true;
717 updateBoard = true;
718 }
719
720 if (updateBoard)
721 {
722 // dbg("updateBoard()\n");
723 G.lautaBGScaled = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
724 Graphics2D gimg = G.lautaBGScaled.createGraphics();
725 gimg.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
726 RenderingHints.VALUE_INTERPOLATION_BICUBIC);
727
728 gimg.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
729 RenderingHints.VALUE_ANTIALIAS_ON);
730
731 gimg.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
732 RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
733
734 gimg.drawImage(G.lautaBG, 0, 0, dim.width, dim.height, null);
735 lauta.paintBackPlate(gimg);
736 lauta.paintBoard(gimg, false);
737 }
738
739 // Get font metrics against current Graphics2D context
740 if (G.metrics == null || scaleChanged)
741 {
742 G.metrics = new FontMetrics[G.numFonts];
743 for (int i = 0; i < G.numFonts; i++)
744 G.metrics[i] = g2.getFontMetrics(G.fonts[i]);
745 }
746
747 // Draw background image, pieces, widgets
748 g2.drawImage(G.lautaBGScaled, 0, 0, null);
749
750 // Use antialiasing when rendering the game elements
751 g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
752 RenderingHints.VALUE_ANTIALIAS_ON);
753
754 g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
755 RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
756
757 widgets.paint(g2);
758
759
760 // Frames per second counter
761 /*
762 g2.setFont(G.fonts[1]);
763 long currTime = new Date().getTime();
764 g2.drawString("fps = "+ ((gameFrames * 1000) / (currTime - startTime)), G.screenDim.width - 120, 20);
765 */
766 gameFrames++;
767 }
768
769 public void startThreads()
770 {
771 dbg("startThreads()\n");
772 if (animThread == null)
773 {
774 animThread = new Thread(this);
775 animEnable = true;
776 animThread.start();
777 }
778 }
779
780 public void stopThreads()
781 {
782 dbg("stopThreads()\n");
783
784 // Stop animations
785 if (animThread != null)
786 {
787 animThread.interrupt();
788 animEnable = false;
789 animThread = null;
790 }
791
792 // Shut down sound manager
793 G.smgr.close();
794 }
795
796 public void mouseEntered(MouseEvent e)
797 {
798 widgets.mouseEntered(e);
799 }
800 public void mouseExited(MouseEvent e)
801 {
802 widgets.mouseExited(e);
803 }
804
805 public void mousePressed(MouseEvent e)
806 {
807 if (widgets.containsObject(aboutBox))
808 aboutBox.mousePressed(e);
809 else
810 widgets.mousePressed(e);
811 }
812
813 public void mouseReleased(MouseEvent e)
814 {
815 if (widgets.containsObject(aboutBox))
816 aboutBox.mouseReleased(e);
817 else
818 widgets.mouseReleased(e);
819 }
820
821 public void mouseClicked(MouseEvent e)
822 {
823 if (!hasFocus())
824 {
825 dbg("Requesting focus.\n");
826 requestFocus();
827 }
828 }
829
830 public void mouseWheelMoved(MouseWheelEvent e)
831 {
832 lauta.mouseWheelMoved(e);
833 }
834
835 public void keyTyped(KeyEvent e) { }
836 public void keyReleased(KeyEvent e) { }
837
838 public void keyPressed(KeyEvent e)
839 {
840 // Handle keyboard input
841 if (widgets.containsObject(aboutBox))
842 aboutBox.keyPressed(e);
843 else
844 widgets.keyPressed(e);
845 }
846
847 public void run()
848 {
849 while (animEnable)
850 {
851 // Progress game animation clock
852 gameUpdates++;
853
854 // Animate components
855 lauta.animate(gameUpdates);
856
857 // Repaint with a frame limiter
858 if (gameUpdates % 4 == 1)
859 repaint();
860
861 // Sleep for a moment
862 try {
863 Thread.sleep(10);
864 }
865 catch (InterruptedException x) {
866 }
867 }
868 }
869
870 class BtnNewGame extends IDMButton
871 {
872 public BtnNewGame(float x, float y)
873 {
874 super(x, y, KeyEvent.VK_ESCAPE, G.fonts[1], "New Game");
875 }
876
877 public void clicked()
878 {
879 startNewGame();
880 }
881 }
882
883 class BtnSwapPiece extends IDMButton
884 {
885 public BtnSwapPiece(float x, float y)
886 {
887 super(x, y, KeyEvent.VK_SPACE, G.fonts[1], "Swap");
888 }
889
890 public void clicked()
891 {
892 lauta.pieceSwapCurrent();
893 }
894 }
895
896 class BtnAbout extends IDMButton
897 {
898 public BtnAbout(float x, float y)
899 {
900 super(x, y, KeyEvent.VK_A, G.fonts[1], "About");
901 }
902
903 public void clicked()
904 {
905 if (!widgets.containsObject(aboutBox))
906 widgets.add(aboutBox);
907 }
908 }
909 }