纯血HarmonyOS ArKTS NETX 5 打造小游戏实践:狼人杀(介绍版(附源文件)
本文章主要介绍狼人杀的功能提供思路,并不能没有真正实现
一、项目整体架构与技术选型
该狼人杀游戏基于ArkTS语言开发,采用HarmonyOS的声明式UI框架,通过组件化设计将游戏逻辑与界面渲染解耦。核心架构包含三大模块:
- 状态管理模块:通过
@State
装饰器实现数据响应式,确保UI随游戏状态自动更新; - 界面渲染模块:利用
Column
、Grid
等容器组件实现分层布局,通过@Builder
修饰符封装可复用UI组件; - 游戏逻辑模块:基于状态机模式管理日夜循环、角色行动与胜负判定,通过枚举类型规范流程控制。
二、状态管理与数据模型详解
1. 核心状态变量解析
@State private players: Player[] = []; // 玩家列表,存储所有角色信息
@State private gamePhase: GamePhase = GamePhase.Night; // 游戏阶段(日夜切换)
@State private nightPhase: NightPhase = NightPhase.Wolf; // 夜间行动顺序
@State private isVoting: boolean = false; // 投票阶段标记
@State private voteResults: Map<number, number> = new Map(); // 投票结果统计
@State private witchHasAntidote: boolean = true; // 女巫解药状态
- 响应式原理:当
players
数组中玩家的isAlive
属性变更时,所有引用该数据的UI组件(如玩家卡片)会自动刷新; - 状态联动:
gamePhase
与nightPhase
配合控制界面显示内容,例如夜间阶段仅显示狼人/预言家/女巫的操作按钮。
2. 玩家模型(Player类)设计
class Player {public isAlive: boolean = true; // 存活状态public role: string; // 角色名称public isWolf: boolean; // 是否为狼人public hasUsedSkill: boolean = false; // 技能使用标记(如猎人开枪)public index: number; // 玩家唯一索引constructor(role: string, index: number) {this.role = role;this.isWolf = role === '狼人';this.index = index;}public kill() {this.isAlive = false;this.isDead = true;}
}
- 角色属性:通过
isWolf
字段区分阵营,role
字段存储角色名称(村民/狼人/预言家等); - 状态切换:
kill()
方法统一处理玩家死亡逻辑,确保UI与数据状态一致。
三、界面布局与组件化实现
1. 玩家列表的左右分栏布局
Scroll() {Grid() {// 左栏玩家(前半部分)ForEach(this.players.slice(0, this.players.length / 2), (player, index) => {GridItem() {this.renderPlayer(player, index, 'left')}})// 右栏玩家(后半部分)ForEach(this.players.slice(this.players.length / 2), (player, index) => {GridItem() {this.renderPlayer(player, index + this.players.length / 2, 'right')}})}.columnsTemplate('1fr 1fr') // 两列等宽.width('100%').height('45%').padding(5)
}
- 布局策略:使用
Grid
容器配合columnsTemplate('1fr 1fr')
实现左右分栏,通过slice
方法分割玩家数组; - 视觉区分:左栏玩家通过
alignItems(HorizontalAlign.Start)
靠左显示,右栏玩家靠右显示,死亡玩家卡片背景色设为#e0e0e0
并降低透明度。
2. 游戏结束弹窗组件
@Builder
renderGameOverDialog() {Dialog() {Column() {Text('游戏结束!').fontSize(24).fontWeight(FontWeight.Bold)Text(`胜利方:${this.winner}`).fontSize(18)Scroll() {Column() {Text('玩家身份:').fontSize(16).fontWeight(FontWeight.Medium)ForEach(this.players, (player) => {Row() {Text(`${player.role} ${player.isWolf ? '🐺' : '👤'}`)Text(player.isAlive ? '存活' : '已淘汰').fontColor(player.isAlive ? '#008000' : '#FF0000')}})}.width('100%').padding(15).backgroundColor('#f0f0f0')}.height('200vp') // 固定弹窗内滚动区域高度Button('重新开始').onClick(() => this.initializeGame())}.width('80%').backgroundColor('#FFFFFF').borderRadius(15)}.maskColor('#00000080') // 半透明遮罩层.alignment(Alignment.Center)
}
- 交互设计:弹窗通过
Dialog
组件实现,半透明遮罩层maskColor
增强沉浸式体验; - 信息展示:使用
Row
布局并排显示玩家角色与存活状态,绿色/红色字体区分存活/死亡状态。
四、游戏核心逻辑与规则实现
1. 夜间行动流程控制
private confirmNightAction() {switch (this.nightPhase) {case NightPhase.Wolf:// 狼人击杀逻辑if (this.selectedPlayer !== -1 && this.players[this.selectedPlayer].isAlive) {this.killedPlayerIndex = this.selectedPlayer;this.addNightMessage(`狼人选择了击杀 ${this.players[this.selectedPlayer].role}`);this.nightPhase = NightPhase.Seer; // 切换至预言家阶段}break;case NightPhase.Seer:// 预言家查验逻辑if (this.selectedPlayer !== -1) {const isWolf = this.players[this.selectedPlayer].isWolf;this.addNightMessage(`预言家查验结果:${isWolf ? '狼人' : '好人'}`);this.nightPhase = NightPhase.Witch; // 切换至女巫阶段}break;case NightPhase.Witch:// 女巫解药/毒药逻辑if (this.selectedPlayer === this.killedPlayerIndex && this.witchHasAntidote) {this.savedPlayerIndex = this.selectedPlayer;this.witchHasAntidote = false;} else if (this.witchHasPoison) {this.poisonedPlayerIndex = this.selectedPlayer;this.witchHasPoison = false;}this.endNight(); // 结束夜间,进入白天break;}
}
- 阶段流转:通过
nightPhase
枚举值控制行动顺序,每个阶段完成后自动切换至下一阶段; - 技能限制:女巫解药仅能救被狼人击杀的玩家,毒药不可对同一目标使用两次。
2. 投票与淘汰机制
private endVote() {// 统计最高票数玩家let maxVotes = 0;let votedPlayerIndex = -1;this.voteResults.forEach((votes, index) => {if (votes > maxVotes) {maxVotes = votes;votedPlayerIndex = index;}});if (votedPlayerIndex !== -1) {this.players[votedPlayerIndex].kill(); // 淘汰玩家this.addDayMessage(`投票结束,${this.players[votedPlayerIndex].role} 被淘汰`);// 猎人技能触发if (this.players[votedPlayerIndex].role === '猎人' && !this.hunterHasShot) {const alivePlayers = this.players.filter(p => p.isAlive && p.index !== votedPlayerIndex);if (alivePlayers.length > 0) {const randomIndex = Math.floor(Math.random() * alivePlayers.length);alivePlayers[randomIndex].kill(); // 随机开枪this.addDayMessage(`猎人开枪带走了 ${alivePlayers[randomIndex].role}`);}}}
}
- 票数处理:使用
Map
统计票数,支持平局判定(tiedPlayers.length > 1
时无人淘汰); - 猎人规则:猎人死亡时若未开枪,自动随机选择存活玩家带走,体现规则严谨性。
五、边界条件与性能优化
1. 胜负判定逻辑
private checkGameStatus() {let wolfCount = 0, villagerCount = 0;this.players.forEach(player => {if (player.isAlive) {wolfCount += player.isWolf ? 1 : 0;villagerCount += !player.isWolf ? 1 : 0;}});if (wolfCount === 0) {this.winner = '好人阵营';this.isGameOver = true;} else if (wolfCount >= villagerCount) {this.winner = '狼人阵营';this.isGameOver = true;}
}
- 实时计算:每次夜间/投票结束后触发,确保游戏状态及时更新;
- 胜利条件:狼人全灭或狼人数量≥好人时,通过
isGameOver
标记触发结束流程。
2. 操作权限控制
private canSelectPlayer(index: number): boolean {if (!this.players[index].isAlive) return false; // 死亡玩家不可选switch (this.nightPhase) {case NightPhase.Wolf: return !this.players[index].isWolf; // 狼人不能杀自己case NightPhase.Seer: return true; // 预言家可查验任意存活玩家case NightPhase.Witch: // 女巫救人/毒人逻辑if (this.killedPlayerIndex === index) return this.witchHasAntidote;return this.witchHasPoison && index !== this.killedPlayerIndex;default: return false;}
}
- 权限过滤:通过
canSelectPlayer
方法统一控制玩家选择权限,避免非法操作; - 阶段限制:狼人阶段不可选择其他狼人,女巫阶段根据解药/毒药状态限制目标。
六、代码可维护性与拓展性设计
- 枚举类型规范:通过
GamePhase
、NightPhase
等枚举明确状态值,避免魔法数字,提升代码可读性; - 复用组件封装:使用
@Builder
修饰符将玩家卡片、按钮组等UI元素封装为可复用组件,减少代码冗余; - 独立方法拆分:将
endNight()
、checkGameStatus()
等逻辑拆分为独立方法,保持build()
方法简洁; - 角色配置拓展:通过修改
roles
数组可快速调整初始角色分配,为添加新角色(如守卫、白痴)预留接口。
七、附:源文件
@Preview
@Component
export struct play_10 {// 游戏状态@State private players: Player[] = [];@State private dayCount: number = 1;@State private gamePhase: GamePhase = GamePhase.Night;@State private nightPhase: NightPhase = NightPhase.Wolf;@State private gameStatus: GameStatus = GameStatus.Playing;@State private message: string = '游戏开始!请等待夜晚降临...';@State private selectedPlayer: number = -1;@State private killedPlayerIndex: number = -1;@State private savedPlayerIndex: number = -1;@State private poisonedPlayerIndex: number = -1;@State private isVoting: boolean = false;@State private voteResults: Map<number, number> = new Map();@State private isGameOver: boolean = false;@State private winner: string = '';@State private currentPlayerIndex: number = -1;@State private playerVoted: boolean = false;@State private nightMessages: string[] = [];@State private dayMessages: string[] = [];// 游戏配置private readonly roles = ['村民', '村民', '村民','狼人', '狼人','预言家', '女巫', '猎人'];// 技能状态@State private witchHasAntidote: boolean = true;@State private witchHasPoison: boolean = true;@State private hunterHasShot: boolean = false;build() {Column() {// 游戏标题和状态(居中显示)Column() {Text('🐺 狼人杀游戏').fontSize(24).fontWeight(FontWeight.Bold).margin({ bottom: 5 })Text(`第${this.dayCount}天 ${this.getPhaseText()}`).fontSize(18).margin({ bottom: 15 })}.width('100%').alignItems(HorizontalAlign.Center)// 游戏消息区域(居中显示)Scroll() {Column() {ForEach(this.getGameMessages(), (msg: string, index: number) => {Text(msg).fontSize(14).margin({ bottom: 5 }).width('90%').textAlign(TextAlign.Start)})}.width('100%').padding(10)}.width('100%').height('25%').backgroundColor('#f5f5f5').borderRadius(10).margin({ bottom: 15 })// 玩家列表(左右分栏显示)Scroll() {Grid() {// 左栏玩家ForEach(this.players.slice(0, this.players.length / 2), (player: Player, index: number) => {GridItem(){this.renderPlayer(player, index, 'left')}})// 右栏玩家ForEach(this.players.slice(this.players.length / 2), (player: Player, index: number) => {GridItem(){this.renderPlayer(player, index + this.players.length / 2, 'right')}})}.columnsTemplate('1fr 1fr').width('100%').height('45%').padding(5)}.width('100%')// 操作按钮(居中显示)if (!this.isGameOver) {this.renderActionButtons()} else {this.renderGameOverScreen()}}.width('100%').height('100%').padding(10).onAppear(() => {this.initializeGame();})}// 获取游戏消息private getGameMessages(): string[] {if (this.gamePhase === GamePhase.Night) {return this.nightMessages;} else {return this.dayMessages;}}// 渲染玩家信息(支持左右分栏)@BuilderrenderPlayer(player: Player, index: number, side: string) {Column() {// 玩家角色和状态Column() {Text(`${player.role} ${player.isWolf ? '🐺' : '👤'}`).fontSize(14).fontWeight(FontWeight.Medium).margin({ bottom: 5 })Text(player.isAlive ? '存活' : '已淘汰').fontSize(12).fontColor(player.isAlive ? '#008000' : '#FF0000')}.width('90%').alignItems(HorizontalAlign.Start).padding(10).backgroundColor(player.isAlive ? '#f0f0f0' : '#e0e0e0').borderRadius(10).margin({ bottom: 10 })// 操作按钮(根据游戏阶段显示)if (this.canSelectPlayer(index)) {Button(this.getActionButtonText(index)).onClick(() => {this.selectPlayer(index);}).width('90%').backgroundColor(this.selectedPlayer === index ? '#007DFF' : '#f5f5f5').fontColor(this.selectedPlayer === index ? '#FFFFFF' : '#212121').margin({ top: 5 })} else if (this.isVoting && player.isAlive) {Button('投票').onClick(() => {this.castVote(index);}).width('90%').backgroundColor(this.selectedPlayer === index ? '#007DFF' : '#f5f5f5').fontColor(this.selectedPlayer === index ? '#FFFFFF' : '#212121').margin({ top: 5 })}}.width(side === 'left' ? '45%' : '45%').alignItems(side === 'left' ? HorizontalAlign.Start : HorizontalAlign.End)}// 渲染操作按钮(居中显示)@BuilderrenderActionButtons() {Column() {// 夜间操作按钮if (this.gamePhase === GamePhase.Night) {Row() {Button('确认行动').onClick(() => {this.confirmNightAction();}).width('40%').margin({ right: '5%' })Button('跳过').onClick(() => {this.skipNightAction();}).width('40%')}.width('90%').margin({ top: 10 })}// 白天操作按钮else if (this.gamePhase === GamePhase.Day) {if (!this.isVoting) {Button('开始投票').onClick(() => {this.startVote();}).width('60%').margin({ top: 10 })} else {Button('结束投票').onClick(() => {this.endVote();}).width('60%').margin({ top: 10 })}}}.width('100%').alignItems(HorizontalAlign.Center)}// 渲染游戏结束界面(居中显示)@BuilderrenderGameOverScreen() {Column() {Text('游戏结束!').fontSize(28).fontWeight(FontWeight.Bold).margin({ bottom: 15 })Text(`胜利方:${this.winner}`).fontSize(22).margin({ bottom: 20 })// 显示所有玩家身份Column() {Text('玩家身份:').fontSize(18).width('60%').height(45).borderRadius(25).backgroundColor('#4CAF50').fontColor('#FFFFFF').onClick(() => {this.initializeGame();})}.width('80%').padding(20).borderRadius(16).backgroundColor('#FFFFFF').shadow({ color: '#00000020', radius: 10, offsetX: 0, offsetY: 4 }) // 添加阴影效果.alignItems(HorizontalAlign.Center)}.width('100%').height('100%').justifyContent(FlexAlign.Center).alignItems(HorizontalAlign.Center)}// 初始化游戏private initializeGame() {this.players = [];this.dayCount = 1;this.gamePhase = GamePhase.Night;this.nightPhase = NightPhase.Wolf;this.gameStatus = GameStatus.Playing;this.message = '游戏开始!请等待夜晚降临...';this.selectedPlayer = -1;this.killedPlayerIndex = -1;this.savedPlayerIndex = -1;this.poisonedPlayerIndex = -1;this.isVoting = false;this.voteResults = new Map();this.isGameOver = false;this.winner = '';this.currentPlayerIndex = -1;this.playerVoted = false;this.nightMessages = ['游戏开始!请等待夜晚降临...'];this.dayMessages = [];// 重置技能状态this.witchHasAntidote = true;this.witchHasPoison = true;this.hunterHasShot = false;// 随机分配角色this.roles.forEach((role,index) => {this.players.push(new Player(role,index));});// 随机打乱玩家顺序this.players.sort(() => Math.random() - 0.5);// 开始第一个夜晚this.addNightMessage('夜晚降临,请闭眼...');}// 添加夜间消息private addNightMessage(message: string) {this.nightMessages.push(message);this.message = message;}// 添加白天消息private addDayMessage(message: string) {this.dayMessages.push(message);this.message = message;}// 确认夜间行动private confirmNightAction() {// 根据当前夜间阶段处理行动switch (this.nightPhase) {case NightPhase.Wolf:if (this.selectedPlayer !== -1 && this.players[this.selectedPlayer].isAlive) {this.killedPlayerIndex = this.selectedPlayer;this.addNightMessage(`狼人选择了击杀 ${this.players[this.selectedPlayer].role}`);this.selectedPlayer = -1;this.nightPhase = NightPhase.Seer;} else {this.addNightMessage('请选择要击杀的玩家!');}break;case NightPhase.Seer:if (this.selectedPlayer !== -1 && this.players[this.selectedPlayer].isAlive) {const isWolf = this.players[this.selectedPlayer].isWolf;this.addNightMessage(`预言家查验 ${this.players[this.selectedPlayer].role} 是 ${isWolf ? '狼人' : '好人'}`);this.selectedPlayer = -1;this.nightPhase = NightPhase.Witch;} else {this.addNightMessage('请选择要查验的玩家!');}break;case NightPhase.Witch:// 女巫可以救人或毒人if (this.selectedPlayer !== -1 && this.players[this.selectedPlayer].isAlive) {// 检查是否是被击杀的玩家if (this.selectedPlayer === this.killedPlayerIndex && this.witchHasAntidote) {this.savedPlayerIndex = this.selectedPlayer;this.witchHasAntidote = false;this.addNightMessage(`女巫使用了解药,救活了 ${this.players[this.selectedPlayer].role}`);} else if (this.selectedPlayer !== this.killedPlayerIndex && this.witchHasPoison) {this.poisonedPlayerIndex = this.selectedPlayer;this.witchHasPoison = false;this.addNightMessage(`女巫使用了毒药,毒死了 ${this.players[this.selectedPlayer].role}`);}}this.selectedPlayer = -1;// 结束夜晚,进入白天this.endNight();break;}}// 跳过夜间行动private skipNightAction() {switch (this.nightPhase) {case NightPhase.Wolf:this.addNightMessage('狼人今晚没有行动');this.nightPhase = NightPhase.Seer;break;case NightPhase.Seer:this.addNightMessage('预言家今晚没有查验');this.nightPhase = NightPhase.Witch;break;case NightPhase.Witch:// 直接结束夜晚this.endNight();break;}this.selectedPlayer = -1;}// 结束夜晚,进入白天private endNight() {// 处理夜间结果let nightResult = `天亮了,昨晚`;// 处理女巫的解药if (this.savedPlayerIndex !== -1) {nightResult += '平安夜';} else {// 处理击杀和毒杀const killedPlayers: number[] = [];if (this.killedPlayerIndex !== -1) {killedPlayers.push(this.killedPlayerIndex);}if (this.poisonedPlayerIndex !== -1 && this.poisonedPlayerIndex !== this.killedPlayerIndex) {killedPlayers.push(this.poisonedPlayerIndex);}if (killedPlayers.length > 0) {nightResult += '死亡的是:';killedPlayers.forEach(index => {this.players[index].kill();nightResult += `${this.players[index].role} `;});} else {nightResult += '平安夜';}}this.addDayMessage(nightResult);// 重置夜间状态this.gamePhase = GamePhase.Day;this.nightPhase = NightPhase.Wolf;this.killedPlayerIndex = -1;this.savedPlayerIndex = -1;this.poisonedPlayerIndex = -1;// 检查游戏是否结束this.checkGameStatus();}// 开始投票private startVote() {this.isVoting = true;this.addDayMessage('开始投票,请选择要投票的玩家');this.voteResults.clear();this.playerVoted = false;}// 投票private castVote(playerIndex: number) {if (this.players[playerIndex].isAlive && !this.playerVoted) {this.selectedPlayer = playerIndex;this.playerVoted = true;// 记录投票结果if (this.voteResults.has(playerIndex)) {this.voteResults.set(playerIndex, this.voteResults.get(playerIndex)! + 1);} else {this.voteResults.set(playerIndex, 1);}this.addDayMessage(`你投票给了 ${this.players[playerIndex].role}`);}}// 结束投票private endVote() {if (this.voteResults.size === 0) {this.addDayMessage('没有人被投票,白天结束');} else {// 找出得票最多的玩家let maxVotes = 0;let votedPlayerIndex = -1;let tiedPlayers: number[] = [];this.voteResults.forEach((votes, index) => {if (votes > maxVotes) {maxVotes = votes;votedPlayerIndex = index;tiedPlayers = [index];} else if (votes === maxVotes) {tiedPlayers.push(index);}});// 处理投票结果if (tiedPlayers.length > 1) {this.addDayMessage('投票平局,没有人被淘汰');} else if (votedPlayerIndex !== -1) {this.players[votedPlayerIndex].kill();this.addDayMessage(`投票结束,${this.players[votedPlayerIndex].role} 被投票出局`);// 处理猎人技能if (this.players[votedPlayerIndex].role === '猎人' && !this.hunterHasShot) {this.hunterHasShot = true;// 随机选择一名存活玩家开枪const alivePlayers = this.players.filter(p => p.isAlive && p.index !== votedPlayerIndex);if (alivePlayers.length > 0) {const randomIndex = Math.floor(Math.random() * alivePlayers.length);const shotPlayer = alivePlayers[randomIndex];shotPlayer.kill();this.addDayMessage(`猎人开枪带走了 ${shotPlayer.role}`);} else {this.addDayMessage('猎人没有可开枪的目标');}}}}this.isVoting = false;this.selectedPlayer = -1;this.playerVoted = false;// 检查游戏是否结束this.checkGameStatus();// 如果游戏继续,进入下一个夜晚if (this.gameStatus === GameStatus.Playing) {this.dayCount++;this.gamePhase = GamePhase.Night;this.nightMessages = [];this.addNightMessage(`第${this.dayCount}天夜晚降临,请闭眼...`);}}// 检查游戏状态private checkGameStatus() {// 计算存活的狼人和好人数量let wolfCount = 0;let villagerCount = 0;this.players.forEach(player => {if (player.isAlive) {if (player.isWolf) {wolfCount++;} else {villagerCount++;}}});// 判断胜利条件if (wolfCount === 0) {this.gameStatus = GameStatus.VillagersWin;this.winner = '好人阵营';this.isGameOver = true;} else if (wolfCount >= villagerCount) {this.gameStatus = GameStatus.WolvesWin;this.winner = '狼人阵营';this.isGameOver = true;}}// 选择玩家private selectPlayer(index: number) {this.selectedPlayer = index;}// 判断是否可以选择玩家private canSelectPlayer(index: number): boolean {if (!this.players[index].isAlive) {return false;}switch (this.nightPhase) {case NightPhase.Wolf:return !this.players[index].isWolf;case NightPhase.Seer:return true;case NightPhase.Witch:// 女巫可以救被击杀的玩家(如果有解药)或毒其他玩家(如果有毒药)if (this.killedPlayerIndex !== -1 && index === this.killedPlayerIndex) {return this.witchHasAntidote;}return this.witchHasPoison && index !== this.killedPlayerIndex;default:return false;}}// 获取行动按钮文本private getActionButtonText(index: number): string {switch (this.nightPhase) {case NightPhase.Wolf:return '击杀';case NightPhase.Seer:return '查验';case NightPhase.Witch:// 如果是被击杀的玩家,显示"救",否则显示"毒"if (this.killedPlayerIndex !== -1 && index === this.killedPlayerIndex) {return '救';}return '毒';default:return '选择';}}// 获取当前阶段文本private getPhaseText(): string {if (this.isGameOver) {return '游戏结束';}if (this.gamePhase === GamePhase.Night) {switch (this.nightPhase) {case NightPhase.Wolf:return '狼人行动';case NightPhase.Seer:return '预言家行动';case NightPhase.Witch:return '女巫行动';default:return '夜晚';}} else {if (this.isVoting) {return '投票阶段';}return '白天讨论';}}
}// 玩家类
class Player {public isAlive: boolean = true;public isDead: boolean = false;public role: string;public isWolf: boolean;public hasUsedSkill: boolean = false; // 技能是否已使用(如猎人的枪)public index: number; // 玩家索引constructor(role: string, index: number) {this.role = role;this.isWolf = role === '狼人';this.index = index;}public kill() {this.isAlive = false;this.isDead = true;}
}// 游戏阶段枚举
enum GamePhase {Night,Day
}// 夜间阶段枚举
enum NightPhase {Wolf,Seer,Witch
}// 游戏状态枚举
enum GameStatus {Playing,VillagersWin,WolvesWin
}