基于Java与WebSocket实战开发交互式斗地主(前端篇 一)
基于Java与WebSocket实战开发交互式斗地主(概括篇)
目录
目录
4、styled-components和@react-spring/web说明
说明:本文主要介绍前端的几个组件,主要实现了“卡牌显示和选择、玩家手牌管理、游戏控制按钮、响应式布局。
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":定义了可以在命令行中运行的脚本命令。这里列出了四个脚本:
"start": "react-scripts start":启动开发服务器,用于开发和测试。"build": "react-scripts build":构建生产环境的静态文件。"test": "react-scripts test":运行测试脚本。"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的一些关键特性:
组件级样式隔离:styled-components通过使用JavaScript标签模板字符串的方式,为每个React组件提供独立的样式作用域,避免了全局CSS中常见的命名冲突问题。
动态样式:可以使用JavaScript表达式和组件的props来动态生成样式,使得styled-components非常适合用于创建主题和动态样式。
服务器端渲染(SSR):styled-components支持服务器端渲染,可以在服务器上预先渲染样式,然后将其发送到客户端,有助于提高首屏加载速度。
与现有CSS库的兼容性:styled-components可以与CSS模块、Tailwind CSS等其他CSS解决方案一起使用,因为它可以接受原始HTML元素的属性。
主题提供:styled-components提供了ThemeProvider组件,允许在应用中全局定义和使用主题。
@react-spring/web 库:
@react-spring/web 是一个用于构建交互式、数据驱动和动画UI组件的库。
以下是@react-spring/web的一些关键特性:
Hooks API:@react-spring/web提供了Hooks API,如
useSpring和useSprings,使得在React组件中创建动画变得简单。动画控制:可以控制动画的起始和结束状态,以及动画的配置,如张力(tension)和摩擦(friction)。
响应式动画:@react-spring/web允许根据用户的交互或其他数据变化来动态控制动画。
集成多种元素:可以为HTML、SVG、Native Elements、Three.js等制作动画。
高级API:除了基本的动画控制,@react-spring/web还提供了如
useTrail、Controller、useResize和useInView等高级API,用于更复杂的动画场景。性能优化:@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、说明
小编写文章时是站在没做过实战项目的角度编写的,文章有点啰嗦 大佬们见怪不怪。但主要目的是让读者了解相关库的存在和功能作用,不一定要记住,但可以了解。
「智能机器人开发者大赛」官方平台,致力于为开发者和参赛选手提供赛事技术指导、行业标准解读及团队实战案例解析;聚焦智能机器人开发全栈技术闭环,助力开发者攻克技术瓶颈,促进软硬件集成、场景应用及商业化落地的深度研讨。 加入智能机器人开发者社区iRobot Developer,与全球极客并肩突破技术边界,定义机器人开发的未来范式!
更多推荐



所有评论(0)