How To Create Memory Game in Java Swing Using NetBeans
In this Java Tutorial we will see How to Create a memory card game using Java Swing with Netbeans.
The Game Features:
- Multiple Difficulty Levels: 4x4, 6x6, and 8x8 grid sizes
- Card Matching: Automatic pair detection.
- Move counter and timer: Track your progress and improve your memory skills.
What We Are Gonna Use In This Project:
- Java Programming Language.- NetBeans Editor.
Project Source Code:
/**
* Initializes the game timer that updates every second
*/
private void initializeTimer() {
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
elapsedTime++;
int minutes = elapsedTime / 60;
int seconds = elapsedTime % 60;
SwingUtilities.invokeLater(() ->
timerLabel.setText(String.format("Time: %02d:%02d", minutes, seconds)));
}
}, 0, 1000);
}
/**
* Method to initialize the game
*/
private void initializeGame() {
// Reset game state
timer.cancel();
timer.purge();
elapsedTime = 0;
timerLabel.setText("Time: 00:00");
moves = 0;
movesLabel.setText("Moves: 0");
// Clear the grid and flipped cards
gridPanel.removeAll();
flippedCards.clear();
// Create new cards
cards = new Card[gridSize * gridSize];
List<String> iconList = new ArrayList<>(
Arrays.asList(ICONS).subList(0, (gridSize * gridSize) / 2)
);
iconList.addAll(iconList); // Duplicate icons for pairs
Collections.shuffle(iconList);
// Initialize cards and add them to the grid
IntStream.range(0, cards.length).forEach(i -> {
cards[i] = new Card(iconList.get(i), COLORS[i % COLORS.length]);
gridPanel.add(cards[i]);
});
// Refresh the grid and restart the timer
gridPanel.revalidate();
gridPanel.repaint();
initializeTimer();
SwingUtilities.invokeLater(this::updateCardSizes);
}
/**
* Creates a panel to display game statistics (moves and time)
*/
private JPanel createStatsPanel() {
// Create a panel for game statistics
JPanel statsPanel = new JPanel();
statsPanel.setLayout(new BoxLayout(statsPanel, BoxLayout.Y_AXIS));
statsPanel.setOpaque(false);
statsPanel.setBorder(BorderFactory.createCompoundBorder(
new LineBorder(new Color(255, 255, 255, 50), 1, true),
new EmptyBorder(15, 15, 15, 15)
));
// Add title and stats labels to the panel
JLabel statsTitle = createLabel("Game Stats", 20, Font.BOLD);
statsPanel.add(statsTitle);
statsPanel.add(Box.createVerticalStrut(10));
movesLabel = createLabel("Moves: 0", 18, Font.PLAIN);
timerLabel = createLabel("Time: 00:00", 18, Font.PLAIN);
statsPanel.add(movesLabel);
statsPanel.add(Box.createVerticalStrut(5));
statsPanel.add(timerLabel);
statsPanel.add(Box.createVerticalStrut(5));
return statsPanel;
}
/**
* Method called when all cards are matched and the game is won
*/
private void gameWon() {
// Stop the game timer
timer.cancel();
// Show the congratulations dialog
SwingUtilities.invokeLater(() -> {
showCongratulationsDialog();
});
}
/**
* Method to display the congratulations dialog when the game is won
*/
private void showCongratulationsDialog() {
// Create a modal dialog
JDialog dialog = new JDialog(this, "Congratulations!", true);
dialog.setLayout(new BorderLayout());
dialog.setSize(300, 200);
dialog.setLocationRelativeTo(this);
// Create a custom panel with gradient background
JPanel panel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(
RenderingHints.KEY_RENDERING,
RenderingHints.VALUE_RENDER_QUALITY
);
int w = getWidth(), h = getHeight();
Color color1 = new Color(41, 128, 185);
Color color2 = new Color(0, 0, 0);
GradientPaint gp = new GradientPaint(0, 0, color1, w, h, color2);
g2d.setPaint(gp);
g2d.fillRect(0, 0, w, h);
};
};
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
panel.setBorder(new EmptyBorder(20, 20, 20, 20));
// Create and add labels for congratulations and stats
JLabel congratsLabel = createLabel("Puzzle Solved!", 24, Font.BOLD);
JLabel statsLabel = createLabel("Moves: " + moves + " | Time: " +
formatTime(elapsedTime), 16, Font.PLAIN);
// Create a close button
JButton closeButton = createButton("Close", new Color(231, 76, 60));
closeButton.addActionListener(e -> dialog.dispose());
// Add components to the panel
panel.add(congratsLabel);
panel.add(Box.createVerticalStrut(10));
panel.add(statsLabel);
panel.add(Box.createVerticalStrut(20));
panel.add(closeButton);
// Add the panel to the dialog and display it
dialog.add(panel, BorderLayout.CENTER);
dialog.setVisible(true);
}
/**
* Method to check for a match between flipped cards
*/
private void checkMatch() {
// Increment the number of moves and update the moves label
moves++;
movesLabel.setText("Moves: " + moves);
// Get the two flipped cards
Card card1 = flippedCards.get(0);
Card card2 = flippedCards.get(1);
// Check if the icons of the two cards match
if (card1.icon.equals(card2.icon)) {
// Cards match
// Mark both cards as matched
card1.isMatched = true;
card2.isMatched = true;
// Start match animation for both cards
card1.matchAnimation();
card2.matchAnimation();
// Clear the list of flipped cards
flippedCards.clear();
// Check if all cards are matched, if so, end the game
if (Arrays.stream(cards).allMatch(card -> card.isMatched)) {
gameWon();
}
} else{
// Cards don't match
// Schedule a timer to flip the cards back after a 1-second delay
Timer flipBackTimer = new Timer();
flipBackTimer.schedule(new TimerTask() {
@Override
public void run() {
// Reset both cards to their original state
card1.reset();
card2.reset();
// Clear the list of flipped cards
flippedCards.clear();
}
}, 1000); // 1000 milliseconds = 1 second
}
}
/**
* Updates the grid size based on selected difficulty level
*/
private void setGridSize(String difficulty) {
gridSize = switch (difficulty) {
case "4x4" -> 4;
case "6x6" -> 6;
case "8x8" -> 8;
default -> 4;
};
gridPanel.setLayout(new GridLayout(gridSize, gridSize, 0, 0));
}
/**
* Method to format the elapsed time in MM:SS format
*/
private String formatTime(int seconds) {
int minutes = seconds / 60;
int remainingSeconds = seconds % 60;
return String.format("%02d:%02d", minutes, remainingSeconds);
}
// **** Inner class representing a card in the memory game
private class Card extends JPanel {
// Properties of the card
private String icon; // The icon (or symbol) displayed on the card when flipped
private Color color; // The background color of the card when flipped
private boolean isFlipped = false; // Indicates if the card is currently flipped
private boolean isMatched = false; // Indicates if the card has been matched
private float rotation = 0; // Current rotation angle for flip animation
private float scale = 1.0f; // Current scale factor for bounce and match animations
private java.util.Timer animationTimer; // Timer for controlling animations
// List to store particle effects
private List<Particle> particles = new ArrayList<>();
// Constructor for the Card class
public Card(String icon, Color color) {
this.icon = icon;
this.color = color;
// Set background color based on dark mode setting
setBackground(isDarkMode ? new Color(44, 62, 80) : BUTTON_PRIMARY);
// Set border color based on dark mode setting
setBorder(BorderFactory.createLineBorder(
isDarkMode ? MAIN_COLOR : BUTTON_SECONDARY,
2, true)
);
// Add mouse listener for card flipping
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// Only flip the card if it's not already flipped, not matched, and less than 2 cards are flipped
if (!isFlipped && !isMatched && flippedCards.size() < 2) {
flip();
}
}
});
}
// Method to reset the card state
public void reset() {
if (animationTimer != null) {
animationTimer.cancel(); // Stop any ongoing animations
}
isFlipped = false;
isMatched = false;
rotation = 0;
scale = 1.0f;
// Reset background color based on dark mode setting
setBackground(isDarkMode ? new Color(44, 62, 80) : BUTTON_PRIMARY);
repaint(); // Redraw the card
}
// Method to flip the card
public void flip() {
isFlipped = true;
flippedCards.add(this); // Add this card to the list of flipped cards
if (animationTimer != null) {
animationTimer.cancel(); // Stop any ongoing animations
}
animationTimer = new Timer();
// Start flip animation
animationTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
rotation += 10; // Increase rotation by 10 degrees each frame
if (rotation >= 360) {
rotation = 360;
animationTimer.cancel(); // Stop animation when full rotation is complete
if (flippedCards.size() == 2) {
checkMatch(); // Check for a match if two cards are flipped
}
}
repaint(); // Redraw the card
}
}, 0, 16); // Run every 16ms
}
// Method for match animation
public void matchAnimation() {
if (animationTimer != null) {
animationTimer.cancel(); // Stop any ongoing animations
}
animationTimer = new Timer();
final float[] scales = {1.2f, 0.9f, 1.1f, 1.0f}; // Scale factors for match effect
final int[] durations = {150, 100, 100, 100}; // Durations for each scale change
// Schedule each scale change
for (int i = 0; i < scales.length; i++) {
final int index = i;
animationTimer.schedule(new TimerTask() {
@Override
public void run() {
scale = scales[index];
repaint(); // Redraw the card
if (index == scales.length - 1) {
startParticleEffect(); // Start particle effect after last scale change
}
}
}, Arrays.stream(durations).limit(i + 1).sum()); // Schedule based on cumulative duration
}
}
// Method to start particle effect animation
private void startParticleEffect() {
synchronized (particles) {
particles.clear(); // Clear any existing particles
for (int i = 0; i < 500; i++) {
particles.add(new Particle(getWidth() / 2, getHeight() / 2)); // Create new particles
}
}
Timer particleTimer = new Timer();
// Animate particles
particleTimer.scheduleAtFixedRate(new TimerTask() {
int frames = 0;
@Override
public void run() {
synchronized (particles) {
particles.forEach(Particle::update); // Update each particle's position
}
SwingUtilities.invokeLater(() -> repaint()); // Redraw the card
frames++;
if (frames > 60) { // Stop animation after 60 frames (1 second at 60 FPS)
particleTimer.cancel();
synchronized (particles) {
particles.clear(); // Remove all particles
}
SwingUtilities.invokeLater(() -> repaint()); // Final redraw
}
}
}, 0, 16); // Run every 16ms
}
// Override paintComponent to draw the card
@Override
protected void paintComponent(Graphics g) {
// Call the superclass method to ensure proper painting
super.paintComponent(g);
// Cast Graphics to Graphics2D for more advanced rendering options
Graphics2D g2d = (Graphics2D) g;
// Enable antialiasing for smoother rendering of shapes and text
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON
);
// Get the current width and height of the component
int width = getWidth();
int height = getHeight();
// Save the current transformation for later restoration
AffineTransform oldTransform = g2d.getTransform();
// Apply rotation and scale transformations
// First, move the origin to the center of the component
g2d.translate(width / 2, height / 2);
// Apply rotation (convert degrees to radians)
g2d.rotate(Math.toRadians(rotation));
// Apply scaling
g2d.scale(scale, scale);
// Move the origin back to the top-left corner
g2d.translate(-width / 2, -height / 2);
if (rotation < 90) {
// Unflipped state (back of the card)
// Fill the card with the background color
g2d.setColor(getBackground());
g2d.fillRoundRect(0, 0, width, height, 15, 15);
// Draw the "?" symbol
g2d.setColor(isDarkMode ? Color.LIGHT_GRAY : Color.WHITE);
g2d.setFont(new Font("Arial", Font.BOLD, 48));
g2d.drawString("?", width / 2 - 15, height / 2 + 15);
} else{
// Flipped state (front of the card)
// Fill the card with its color
g2d.setColor(color);
g2d.fillRoundRect(0, 0, width, height, 15, 15);
// Draw the icon (symbol) on the card
g2d.setColor(Color.WHITE);
g2d.setFont(new Font("Arial", Font.BOLD, 48));
FontMetrics fm = g2d.getFontMetrics();
// Calculate position to center the icon
int x = (width - fm.stringWidth(icon)) / 2;
int y = ((height - fm.getHeight()) / 2) + fm.getAscent();
g2d.drawString(icon, x, y);
}
// Restore the original transformation
g2d.setTransform(oldTransform);
// Draw particles (for animation effects)
// Create a copy of the particles list to avoid concurrent modification
List<Particle> particlesCopy;
synchronized (particles) {
particlesCopy = new ArrayList<>(particles);
}
// Draw each particle
particlesCopy.forEach(p -> {
if (p != null) {
p.draw(g2d);
}
});
}
}
// Inner class representing a particle for visual effects
private class Particle {
// Properties of the particle
private double x, y; // Current position of the particle
private double vx, vy; // Velocity of the particle in x and y directions
private double size; // Size of the particle
private Color color; // Color of the particle
private double alpha = 1.0; // Transparency of the particle (1.0 = fully opaque)
// Constructor for the Particle class
public Particle(int x, int y) {
// Set the initial position of the particle
this.x = x;
this.y = y;
// Calculate a random angle and speed for the particle's movement
double angle = Math.random() * 2 * Math.PI; // Random angle in radians
double speed = 1 + Math.random() * 3; // Random speed between 1 and 4
// Calculate the x and y components of the velocity based on angle and speed
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
// Set a random size for the particle between 2 and 6
this.size = 2 + Math.random() * 4;
// Assign a random color to the particle
this.color = new Color(
(float) Math.random(), // Random red component
(float) Math.random(), // Random green component
(float) Math.random() // Random blue component
);
}
// Method to update particle position and properties
public void update() {
// Update the particle's position based on its velocity
x += vx;
y += vy;
// Apply a gravity effect by increasing the downward velocity
vy += 0.1; // Gravity effect
// Shrink the particle over time
size *= 0.97; // Reduce size by 3% each update
// Fade out the particle over time
alpha *= 0.97; // Reduce alpha by 3% each update
}
// Method to draw the particle
public void draw(Graphics2D g2d) {
// Set the transparency (alpha) for drawing the particle
g2d.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, (float) alpha)
);
// Set the color of the particle
g2d.setColor(color);
// Draw the particle as a filled circle (ellipse)
// The position is adjusted to center the particle on its x,y coordinates
g2d.fill(new Ellipse2D.Double(x - size / 2, y - size / 2, size, size));
}
}
The Final Result:
if you want the full source code click on the download button below
disclaimer: you will get the source code with the database script and to make it work in your machine is your responsibility and to debug any error/exception is your responsibility this project is for the students who want to see an example and read the code not to get and run.
More Java Projects:
Download Projects Source Code