基于Java与WebSocket实战开发交互式斗地主(概括篇)

目录

目录

1、创建样式文件

2、创建 package.json 文件

3、创建游戏卡牌组件

4、styled-components和@react-spring/web说明

5、创建玩家手牌区域组件

6、创建游戏控制面板组件:

7、创建主游戏组件:

8、更新 App.js 文件:

9、说明

说明:本文主要介绍前端的几个组件,主要实现了“卡牌显示和选择、玩家手牌管理、游戏控制按钮、响应式布局。

1、创建样式文件

        main.css文件通常是一个用于定义前端样式的CSS文件。这个文件的作用和任何其他CSS文件的作用一样,都是为了设置HTML元素的样式,包括颜色、字体、布局等。

/* 全局样式 */
:root {
    --primary-color: #2c3e50;
    --secondary-color: #e74c3c;
    --background-color: #ecf0f1;
    --text-color: #34495e;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Microsoft YaHei', sans-serif;
    background-color: var(--background-color);
    color: var(--text-color);
}

/* 游戏容器 */
.game-container {
    max-width: 1200px;
    margin: 0 auto;
    padding: 20px;
}

/* 响应式设计 */
@media (max-width: 768px) {
    .game-container {
        padding: 10px;
    }
}

2、创建 package.json 文件

        package.json文件是Node.js项目(和使用npm或yarn的前端项目)的配置文件,用于定义项目所需的依赖、脚本和配置信息。

  • "name": "doudizhu-game":定义了项目名称,即斗地主游戏。
  • "version": "1.0.0":定义了项目的版本号1.0.0,这是一个初始版本。
  • "private": true:表示这是一个私有项目,不应该被发布到npm公共仓库。
  • "dependencies":定义了项目依赖的第三方库及其版本。这里列出了项目所需的一些库:
  • "react": "^18.2.0":React库的版本,表示兼容的版本即可以是18.2.0或更高版本,但必须与18.x.x兼容。
  • "react-dom": "^18.2.0":React DOM库的版本,与React库版本相同。
  • "react-scripts": "5.0.1":用于创建React应用的脚本工具,这里指定了特定版本5.0.1。
  • "@material-ui/core": "^4.12.4":Material-UI库的核心部分,用于UI组件。
  • "@material-ui/icons": "^4.11.3":Material-UI库的图标部分。
  • "socket.io-client": "^4.7.1":Socket.IO的客户端库,用于实现实时通信。
  • "styled-components": "^5.3.11":用于CSS-in-JS的库,允许在JavaScript中编写CSS。
  • "scripts":定义了可以在命令行中运行的脚本命令。这里列出了四个脚本:
  1. "start": "react-scripts start":启动开发服务器,用于开发和测试。
  2. "build": "react-scripts build":构建生产环境的静态文件。
  3. "test": "react-scripts test":运行测试脚本。
  4. "eject": "react-scripts eject":将react-scripts配置“弹出”,使得项目不再依赖react-scripts,而是直接使用Webpack等工具进行配置。这个操作是不可逆的,通常不推荐使用。
{
  "name": "doudizhu-game",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "@material-ui/core": "^4.12.4",
    "@material-ui/icons": "^4.11.3",
    "socket.io-client": "^4.7.1",
    "styled-components": "^5.3.11"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
}

3、创建游戏卡牌组件

        用React组件创建Card.jsx,以下是说明和代码。

1. React 和 styled-components:
   import React from 'react';:导入React库,用于构建用户界面。
   import styled from 'styled-components';:导入styled-components库,用于创建CSS-in-JS样式的React组件。
2. @react-spring/web:
  import { useSpring, animated } from '@react-spring/web';:从@react-spring/web库中导入useSpring和animated。useSpring是一个React Hook,用于创建动画,而animated用于包装React组件以使其可以被动画化。
3. Styled Components:
   CardWrapper和CardValue是使用styled-components创建的两个组件,它们分别定义了扑克牌的容器和牌面的值的样式。
4. CardWrapper:
   const CardWrapper = styled(animated.div):创建一个动画化的div元素,用于扑克牌的容器。
5. CardValue:
   const CardValue = styled.div:创建一个div元素,用于显示扑克牌的值和花色。
6. Card组件:
   const Card = ({ value, suit, selected, onClick, animate }) => { ... }:定义Card组件,它接受value(牌的值),suit(牌的花色),selected(是否被选中),onClick(点击事件处理函数),和animate(动画属性)作为props。
7. 颜色逻辑:
   const color = suit === '♥' || suit === '♦' ? 'red' : 'black';:根据花色确定字体颜色,红心和方块为红色,其他为黑色。
8. useSpring动画:
   const springProps = useSpring({ ... });:定义扑克牌选中时的动画属性,包括位移、旋转和缩放。
   const hoverProps = useSpring({ ... });:定义鼠标悬停时的动画属性。
9. 事件处理:
   onMouseEnter和onMouseLeave:处理鼠标悬停和离开事件,触发相应的动画效果。
10. JSX结构:
   <CardWrapper ...>:渲染CardWrapper组件,并传递style属性(包含动画属性),onClick事件处理函数,以及鼠标悬停和离开的事件处理函数。
   <CardValue color={color}>:渲染CardValue组件,并传递color属性和牌的值。
11. 导出:
   export default Card;:将Card组件导出,使其可以在其他文件中使用。

import React from 'react';
import styled from 'styled-components';
import { useSpring, animated } from '@react-spring/web';

const CardWrapper = styled(animated.div)`
  width: 90px;
  height: 120px;
  border-radius: 8px;
  background: white;
  position: relative;
  box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  margin: 0 -15px;
  cursor: pointer;
  transform-origin: center bottom;
`;

const CardValue = styled.div`
  position: absolute;
  top: 10px;
  left: 10px;
  font-size: 24px;
  font-weight: bold;
  color: ${props => props.color};
`;

const Card = ({ value, suit, selected, onClick, animate }) => {
  const color = suit === '♥' || suit === '♦' ? 'red' : 'black';
  
  const springProps = useSpring({
    transform: selected 
      ? 'translateY(-20px) rotate(0deg)' 
      : 'translateY(0) rotate(0deg)',
    scale: selected ? 1.1 : 1,
    config: { tension: 300, friction: 20 }
  });

  const hoverProps = useSpring({
    scale: 1,
    rotate: '0deg',
    config: { tension: 300, friction: 10 }
  });

  return (
    <CardWrapper 
      style={{
        ...springProps,
        ...hoverProps
      }}
      onClick={onClick}
      onMouseEnter={() => hoverProps.scale.start(1.05)}
      onMouseLeave={() => hoverProps.scale.start(1)}
    >
      <CardValue color={color}>
        {value}
        <div>{suit}</div>
      </CardValue>
    </CardWrapper>
  );
};

export default Card;

4、styled-components和@react-spring/web说明

styled-components 库:

        styled-components  基于React的CSS-in-JS库,允许将CSS直接嵌入到JavaScript中,以函数式的方式定义组件样式。通过这种方式可以为每个组件创建高度封装和可维护的样式。以下是styled-components的一些关键特性:

  1. 组件级样式隔离:styled-components通过使用JavaScript标签模板字符串的方式,为每个React组件提供独立的样式作用域,避免了全局CSS中常见的命名冲突问题。

  2. 动态样式:可以使用JavaScript表达式和组件的props来动态生成样式,使得styled-components非常适合用于创建主题和动态样式。

  3. 服务器端渲染(SSR):styled-components支持服务器端渲染,可以在服务器上预先渲染样式,然后将其发送到客户端,有助于提高首屏加载速度。

  4. 与现有CSS库的兼容性:styled-components可以与CSS模块、Tailwind CSS等其他CSS解决方案一起使用,因为它可以接受原始HTML元素的属性。

  5. 主题提供:styled-components提供了ThemeProvider组件,允许在应用中全局定义和使用主题。

@react-spring/web 库:

        @react-spring/web 是一个用于构建交互式、数据驱动和动画UI组件的库。

        以下是@react-spring/web的一些关键特性:

  1. Hooks API:@react-spring/web提供了Hooks API,如useSpringuseSprings,使得在React组件中创建动画变得简单。

  2. 动画控制:可以控制动画的起始和结束状态,以及动画的配置,如张力(tension)和摩擦(friction)。

  3. 响应式动画:@react-spring/web允许根据用户的交互或其他数据变化来动态控制动画。

  4. 集成多种元素:可以为HTML、SVG、Native Elements、Three.js等制作动画。

  5. 高级API:除了基本的动画控制,@react-spring/web还提供了如useTrailControlleruseResizeuseInView等高级API,用于更复杂的动画场景。

  6. 性能优化:@react-spring/web的动画是基于requestAnimationFrame实现的,意味着动画可以平滑运行,而不会影响页面的性能。

        这两个库都是React生态系统中的重要工具,styled-components专注于样式的封装和动态生成,而@react-spring/web专注于动画和交互的实现。

5、创建玩家手牌区域组件(PlayerHand.jsx)

import React from 'react'; // 导入React库
import styled from 'styled-components'; // 导入styled-components库用于样式设计
import Card from './Card'; // 导入Card组件

// 使用styled-components创建一个用于布局玩家手牌的样式组件
const HandWrapper = styled.div`
  display: flex; 
  justify-content: center; 
  align-items: flex-end; 
  padding: 20px;
  min-height: 150px;
`;

// PlayerHand组件,用于显示玩家手中的卡片
const PlayerHand = ({ cards, selectedCards, onCardClick }) => {
  // 使用HandWrapper样式组件包裹卡片
  return (
    <HandWrapper>
      {cards.map((card, index) => ( // 遍历cards数组,为每张卡片生成Card组件
        // 为每个Card组件设置唯一的key
        <Card
          key={`${card.value}-${card.suit}`}
          value={card.value} 
          suit={card.suit} 
          selected={selectedCards.includes(index)} 
          onClick={() => onCardClick(index)} 
        />
      ))}
    </HandWrapper>
  );
};

// 导出PlayerHand组件
export default PlayerHand;

6、创建游戏控制面板组件(GameControls.jsx)

import React from 'react';
import styled from 'styled-components'; 

// 创建一个用于包裹游戏控制按钮的样式组件
const ControlsWrapper = styled.div`
  display: flex; 
  justify-content: center; 
  gap: 20px; 
  padding: 20px; 
`;

// 创建一个按钮样式组件
const Button = styled.button`
  padding: 10px 20px; 
  border-radius: 5px; 
  border: none; 
  background-color: var(--primary-color); 
  color: white; 
  font-size: 16px; 
  cursor: pointer; 
  transition: background-color 0.3s;

  &:hover { /* 鼠标悬停时的样式 */
    background-color: var(--secondary-color); /* 次要主题颜色 */
  }

  &:disabled { /* 禁用状态下的样式 */
    background-color: #ccc; 
    cursor: not-allowed; 
  }
`;

// GameControls组件,用于渲染游戏控制按钮
const GameControls = ({ onPlay, onPass, isMyTurn }) => {
  return (
    <ControlsWrapper>
      <Button onClick={onPlay} disabled={!isMyTurn}>出牌</Button> // 出牌按钮,当不是玩家回合时禁用
      <Button onClick={onPass} disabled={!isMyTurn}>不出</Button> // 不出牌按钮,当不是玩家回合时禁用
    </ControlsWrapper>
  );
};

export default GameControls; // 导出GameControls组件

7、创建主游戏组件(Game.jsx)

import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import PlayerHand from './PlayerHand';
import GameControls from './GameControls';
import GameState from '../services/gameState';
import WebSocketService from '../services/websocket';
import { DealAnimation, PlayAnimation } from './animations/CardAnimation';
import AnimationManager from '../services/animationManager';
import CardPatternValidator, { CardPattern } from '../services/cardPatternValidator';
import EffectsManager from '../services/effectsManager';

const GameWrapper = styled.div`
  display: flex;
  flex-direction: column;
  height: 100vh;
  background-color: var(--background-color);
`;

const GameInfo = styled.div`
  text-align: center;
  padding: 20px;
  font-size: 18px;
  color: var(--text-color);
`;

const Game = () => {
  const [cards, setCards] = useState([]);
  const [selectedCards, setSelectedCards] = useState([]);
  const [isMyTurn, setIsMyTurn] = useState(false);
  const [gameStatus, setGameStatus] = useState('waiting');
  const [lastPlayedCards, setLastPlayedCards] = useState([]);
  const [animatingCards, setAnimatingCards] = useState([]);
  const [lastPattern, setLastPattern] = useState(null);

  useEffect(() => {
    // 连接WebSocket
    WebSocketService.connect('ws://your-server-url/game');

    // 注册游戏状态事件监听
    GameState.on('cardsDealt', ({ cards }) => {
      setCards(cards);
    });

    GameState.on('turnChanged', ({ currentPlayer }) => {
      // 假设 playerId 是当前玩家的ID
      setIsMyTurn(currentPlayer === 'playerId');
    });

    GameState.on('cardsPlayed', ({ playerId, cards }) => {
      setLastPlayedCards(cards);
    });

    GameState.on('gameStatusChanged', ({ status }) => {
      setGameStatus(status);
    });

    // 添加动画事件监听
    AnimationManager.on('deal', handleDealAnimation);
    AnimationManager.on('play', handlePlayAnimation);

    // 清理函数
    return () => {
      GameState.off('cardsDealt');
      GameState.off('turnChanged');
      GameState.off('cardsPlayed');
      GameState.off('gameStatusChanged');
    };
  }, []);

  const handleDealAnimation = (data) => {
    setAnimatingCards(prev => [...prev, {
      ...data.card,
      animationType: 'deal',
      delay: data.index * 100
    }]);
  };

  const handlePlayAnimation = (data) => {
    setAnimatingCards(prev => [...prev, {
      ...data.card,
      animationType: 'play',
      position: data.position
    }]);
  };

  const handleCardClick = (index) => {
    EffectsManager.playSound('button');
    setSelectedCards(prev => 
      prev.includes(index) 
        ? prev.filter(i => i !== index)
        : [...prev, index]
    );
  };

  const validatePlay = (selectedCards) => {
    // 获取选中牌的牌型
    const pattern = CardPatternValidator.getPattern(selectedCards);
    
    // 如果没有上一手牌,可以任意出牌
    if (!lastPlayedCards.length) {
      return pattern !== null;
    }

    // 如果是炸弹或火箭,始终可以出
    if (pattern === CardPattern.ROCKET || pattern === CardPattern.BOMB) {
      return true;
    }

    // 牌型必须相同,且大小要大于上一手牌
    if (pattern !== lastPattern) {
      return false;
    }

    return CardPatternValidator.compareCards(pattern, selectedCards, lastPlayedCards) > 0;
  };

  const handlePlay = () => {
    const selectedCardsData = selectedCards.map(index => cards[index]);
    
    if (!validatePlay(selectedCardsData)) {
      EffectsManager.playSound('alert');
      alert('出牌不符合规则!');
      return;
    }

    const pattern = CardPatternValidator.getPattern(selectedCardsData);
    
    // 播放相应的音效
    if (pattern === CardPattern.ROCKET) {
      EffectsManager.playSound('rocket');
    } else if (pattern === CardPattern.BOMB) {
      EffectsManager.playSound('bomb');
    } else {
      EffectsManager.playSound('play');
    }

    // 播放出牌动画
    AnimationManager.playCards(selectedCardsData, 'center', () => {
      setLastPattern(pattern);
      GameState.playCards(selectedCardsData);
      setSelectedCards([]);
    });
  };

  const handlePass = () => {
    GameState.pass();
    setSelectedCards([]);
  };

  // 处理游戏结束
  const handleGameOver = (data) => {
    const isWinner = data.winner === 'playerId'; // 假设 playerId 是当前玩家的ID
    EffectsManager.playSound(isWinner ? 'win' : 'lose');
    
    // 创建胜利/失败特效
    const gameWrapper = document.querySelector('.game-wrapper');
    if (isWinner) {
      EffectsManager.createParticles(gameWrapper, {
        particleCount: 50,
        colors: ['#FFD700', '#FFA500', '#FF6347'],
        spread: 150
      });
    } else {
      EffectsManager.createFlash(gameWrapper, 'rgba(255, 0, 0, 0.3)');
    }
  };

  useEffect(() => {
    GameState.on('gameOver', handleGameOver);

    return () => {
      GameState.off('gameOver');
    };
  }, []);

  return (
    <GameWrapper className="game-wrapper">
      <GameInfo>
        {gameStatus === 'waiting' && '等待其他玩家加入...'}
        {gameStatus === 'playing' && (isMyTurn ? '轮到你出牌了!' : '等待其他玩家出牌...')}
        {gameStatus === 'finished' && '游戏结束!'}
      </GameInfo>
      
      {lastPlayedCards.length > 0 && (
        <LastPlayedCards>
          上一手牌:
          {lastPlayedCards.map((card, index) => (
            <Card
              key={index}
              value={card.value}
              suit={card.suit}
              disabled
            />
          ))}
        </LastPlayedCards>
      )}

      <PlayerHand
        cards={cards}
        selectedCards={selectedCards}
        onCardClick={handleCardClick}
      />
      
      <GameControls
        onPlay={handlePlay}
        onPass={handlePass}
        isMyTurn={isMyTurn}
      />
      
      {/* 动画层 */}
      {animatingCards.map((card, index) => (
        card.animationType === 'deal' ? (
          <DealAnimation 
            key={`${card.value}-${card.suit}-${index}`}
            delay={card.delay}
            index={index}
          >
            <Card value={card.value} suit={card.suit} />
          </DealAnimation>
        ) : (
          <PlayAnimation
            key={`${card.value}-${card.suit}-${index}`}
            onComplete={() => {
              setAnimatingCards(prev => 
                prev.filter(c => c !== card)
              );
            }}
          >
            <Card value={card.value} suit={card.suit} />
          </PlayAnimation>
        )
      ))}
    </GameWrapper>
  );
};

export default Game;

8、更新 App.js 文件

import React, { useEffect } from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import GameLobby from './components/GameLobby';
import GameRoom from './components/GameRoom';
import Login from './components/Login';
import PrivateRoute from './components/PrivateRoute';
import ErrorBoundary from './components/ErrorBoundary';
import ErrorHandler from './services/errorHandler';
import './styles/main.css';

function App() {
  useEffect(() => {
    // 注册全局错误处理
    const unregister = ErrorHandler.registerHandler((error) => {
      console.error('Global error:', error);
      // 可以在这里添加全局错误提示
    });

    return () => unregister();
  }, []);

  return (
    <ErrorBoundary>
      <BrowserRouter>
        <Routes>
          {/* 公开路由 */}
          <Route path="/login" element={<Login />} />
          
          {/* 受保护的路由 */}
          <Route
            path="/lobby"
            element={
              <PrivateRoute>
                <GameLobby />
              </PrivateRoute>
            }
          />
          <Route
            path="/room/:roomId"
            element={
              <PrivateRoute>
                <GameRoom />
              </PrivateRoute>
            }
          />
          
          {/* 重定向 */}
          <Route path="/" element={<Navigate to="/login" replace />} />
        </Routes>
      </BrowserRouter>
    </ErrorBoundary>
  );
}

export default App;

9、说明

        小编写文章时是站在没做过实战项目的角度编写的,文章有点啰嗦 大佬们见怪不怪。但主要目的是让读者了解相关库的存在和功能作用,不一定要记住,但可以了解。

Logo

「智能机器人开发者大赛」官方平台,致力于为开发者和参赛选手提供赛事技术指导、行业标准解读及团队实战案例解析;聚焦智能机器人开发全栈技术闭环,助力开发者攻克技术瓶颈,促进软硬件集成、场景应用及商业化落地的深度研讨。 加入智能机器人开发者社区iRobot Developer,与全球极客并肩突破技术边界,定义机器人开发的未来范式!

更多推荐