当前位置: 首页 > news >正文

用深度强化学习来玩Flappy Bird

目录

演示视频

核心代码


演示视频

用深度强化学习来玩Flappy Bird

核心代码

import torch.nn as nnclass DeepQNetwork(nn.Module):def __init__(self):super(DeepQNetwork, self).__init__()self.conv1 = nn.Sequential(nn.Conv2d(4, 32, kernel_size=8, stride=4), nn.ReLU(inplace=True))self.conv2 = nn.Sequential(nn.Conv2d(32, 64, kernel_size=4, stride=2), nn.ReLU(inplace=True))self.conv3 = nn.Sequential(nn.Conv2d(64, 64, kernel_size=3, stride=1), nn.ReLU(inplace=True))self.fc1 = nn.Sequential(nn.Linear(7 * 7 * 64, 512), nn.ReLU(inplace=True))self.fc2 = nn.Linear(512, 2)self._create_weights()def _create_weights(self):for m in self.modules():if isinstance(m, nn.Conv2d) or isinstance(m, nn.Linear):nn.init.uniform_(m.weight, -0.01, 0.01)nn.init.constant_(m.bias, 0)def forward(self, input):output = self.conv1(input)output = self.conv2(output)output = self.conv3(output)output = output.view(output.size(0), -1)output = self.fc1(output)output = self.fc2(output)return output
from itertools import cycle
from numpy.random import randint
from pygame import Rect, init, time, display
from pygame.event import pump
from pygame.image import load
from pygame.surfarray import array3d, pixels_alpha
from pygame.transform import rotate
import numpy as npclass FlappyBird(object):init()fps_clock = time.Clock()screen_width = 288screen_height = 512screen = display.set_mode((screen_width, screen_height))display.set_caption('Deep Q-Network Flappy Bird')base_image = load('assets/sprites/base.png').convert_alpha()background_image = load('assets/sprites/background-black.png').convert()pipe_images = [rotate(load('assets/sprites/pipe-green.png').convert_alpha(), 180),load('assets/sprites/pipe-green.png').convert_alpha()]bird_images = [load('assets/sprites/redbird-upflap.png').convert_alpha(),load('assets/sprites/redbird-midflap.png').convert_alpha(),load('assets/sprites/redbird-downflap.png').convert_alpha()]# number_images = [load('assets/sprites/{}.png'.format(i)).convert_alpha() for i in range(10)]bird_hitmask = [pixels_alpha(image).astype(bool) for image in bird_images]pipe_hitmask = [pixels_alpha(image).astype(bool) for image in pipe_images]fps = 30pipe_gap_size = 100pipe_velocity_x = -4# parameters for birdmin_velocity_y = -8max_velocity_y = 10downward_speed = 1upward_speed = -9bird_index_generator = cycle([0, 1, 2, 1])def __init__(self):self.iter = self.bird_index = self.score = 0self.bird_width = self.bird_images[0].get_width()self.bird_height = self.bird_images[0].get_height()self.pipe_width = self.pipe_images[0].get_width()self.pipe_height = self.pipe_images[0].get_height()self.bird_x = int(self.screen_width / 5)self.bird_y = int((self.screen_height - self.bird_height) / 2)self.base_x = 0self.base_y = self.screen_height * 0.79self.base_shift = self.base_image.get_width() - self.background_image.get_width()pipes = [self.generate_pipe(), self.generate_pipe()]pipes[0]["x_upper"] = pipes[0]["x_lower"] = self.screen_widthpipes[1]["x_upper"] = pipes[1]["x_lower"] = self.screen_width * 1.5self.pipes = pipesself.current_velocity_y = 0self.is_flapped = Falsedef generate_pipe(self):x = self.screen_width + 10gap_y = randint(2, 10) * 10 + int(self.base_y / 5)return {"x_upper": x, "y_upper": gap_y - self.pipe_height, "x_lower": x, "y_lower": gap_y + self.pipe_gap_size}def is_collided(self):# Check if the bird touch groundif self.bird_height + self.bird_y + 1 >= self.base_y:return Truebird_bbox = Rect(self.bird_x, self.bird_y, self.bird_width, self.bird_height)pipe_boxes = []for pipe in self.pipes:pipe_boxes.append(Rect(pipe["x_upper"], pipe["y_upper"], self.pipe_width, self.pipe_height))pipe_boxes.append(Rect(pipe["x_lower"], pipe["y_lower"], self.pipe_width, self.pipe_height))# Check if the bird's bounding box overlaps to the bounding box of any pipeif bird_bbox.collidelist(pipe_boxes) == -1:return Falsefor i in range(2):cropped_bbox = bird_bbox.clip(pipe_boxes[i])min_x1 = cropped_bbox.x - bird_bbox.xmin_y1 = cropped_bbox.y - bird_bbox.ymin_x2 = cropped_bbox.x - pipe_boxes[i].xmin_y2 = cropped_bbox.y - pipe_boxes[i].yif np.any(self.bird_hitmask[self.bird_index][min_x1:min_x1 + cropped_bbox.width,min_y1:min_y1 + cropped_bbox.height] * self.pipe_hitmask[i][min_x2:min_x2 + cropped_bbox.width,min_y2:min_y2 + cropped_bbox.height]):return Truereturn Falsedef next_frame(self, action):pump()reward = 0.1terminal = False# Check input actionif action == 1:self.current_velocity_y = self.upward_speedself.is_flapped = True# Update scorebird_center_x = self.bird_x + self.bird_width / 2for pipe in self.pipes:pipe_center_x = pipe["x_upper"] + self.pipe_width / 2if pipe_center_x < bird_center_x < pipe_center_x + 5:self.score += 1reward = 1break# Update index and iterationif (self.iter + 1) % 3 == 0:self.bird_index = next(self.bird_index_generator)self.iter = 0self.base_x = -((-self.base_x + 100) % self.base_shift)# Update bird's positionif self.current_velocity_y < self.max_velocity_y and not self.is_flapped:self.current_velocity_y += self.downward_speedif self.is_flapped:self.is_flapped = Falseself.bird_y += min(self.current_velocity_y, self.bird_y - self.current_velocity_y - self.bird_height)if self.bird_y < 0:self.bird_y = 0# Update pipes' positionfor pipe in self.pipes:pipe["x_upper"] += self.pipe_velocity_xpipe["x_lower"] += self.pipe_velocity_x# Update pipesif 0 < self.pipes[0]["x_lower"] < 5:self.pipes.append(self.generate_pipe())if self.pipes[0]["x_lower"] < -self.pipe_width:del self.pipes[0]if self.is_collided():terminal = Truereward = -1self.__init__()# Draw everythingself.screen.blit(self.background_image, (0, 0))self.screen.blit(self.base_image, (self.base_x, self.base_y))self.screen.blit(self.bird_images[self.bird_index], (self.bird_x, self.bird_y))for pipe in self.pipes:self.screen.blit(self.pipe_images[0], (pipe["x_upper"], pipe["y_upper"]))self.screen.blit(self.pipe_images[1], (pipe["x_lower"], pipe["y_lower"]))image = array3d(display.get_surface())display.update()self.fps_clock.tick(self.fps)return image, reward, terminal
import argparse
import torchfrom src.deep_q_network import DeepQNetwork
from src.flappy_bird import FlappyBird
from src.utils import pre_processingdef get_args():parser = argparse.ArgumentParser("""Implementation of Deep Q Network to play Flappy Bird""")parser.add_argument("--image_size", type=int, default=84, help="The common width and height for all images")parser.add_argument("--saved_path", type=str, default="trained_models")args = parser.parse_args()return argsdef q_test(opt):if torch.cuda.is_available():torch.cuda.manual_seed(123)else:torch.manual_seed(123)if torch.cuda.is_available():model = torch.load("{}/flappy_bird".format(opt.saved_path))else:model = torch.load("{}/flappy_bird".format(opt.saved_path), map_location=lambda storage, loc: storage)model.eval()game_state = FlappyBird()image, reward, terminal = game_state.next_frame(0)image = pre_processing(image[:game_state.screen_width, :int(game_state.base_y)], opt.image_size, opt.image_size)image = torch.from_numpy(image)if torch.cuda.is_available():model.cuda()image = image.cuda()state = torch.cat(tuple(image for _ in range(4)))[None, :, :, :]while True:prediction = model(state)[0]action = torch.argmax(prediction)next_image, reward, terminal = game_state.next_frame(action)next_image = pre_processing(next_image[:game_state.screen_width, :int(game_state.base_y)], opt.image_size,opt.image_size)next_image = torch.from_numpy(next_image)if torch.cuda.is_available():next_image = next_image.cuda()next_state = torch.cat((state[0, 1:, :, :], next_image))[None, :, :, :]state = next_stateif __name__ == "__main__":opt = get_args()q_test(opt)
def get_args():parser = argparse.ArgumentParser("""Implementation of Deep Q Network to play Flappy Bird""")parser.add_argument("--image_size", type=int, default=84, help="The common width and height for all images")parser.add_argument("--batch_size", type=int, default=32, help="The number of images per batch")parser.add_argument("--optimizer", type=str, choices=["sgd", "adam"], default="adam")parser.add_argument("--lr", type=float, default=1e-6)parser.add_argument("--gamma", type=float, default=0.99)parser.add_argument("--initial_epsilon", type=float, default=0.1)parser.add_argument("--final_epsilon", type=float, default=1e-4)parser.add_argument("--num_iters", type=int, default=2000000)parser.add_argument("--replay_memory_size", type=int, default=50000,help="Number of epoches between testing phases")parser.add_argument("--log_path", type=str, default="tensorboard")parser.add_argument("--saved_path", type=str, default="trained_models")args = parser.parse_args()return argsdef train(opt):if torch.cuda.is_available():torch.cuda.manual_seed(123)else:torch.manual_seed(123)model = DeepQNetwork()if os.path.isdir(opt.log_path):shutil.rmtree(opt.log_path)os.makedirs(opt.log_path)writer = SummaryWriter(opt.log_path)optimizer = torch.optim.Adam(model.parameters(), lr=opt.lr)criterion = nn.MSELoss()game_state = FlappyBird()image, reward, terminal = game_state.next_frame(0)image = pre_processing(image[:game_state.screen_width, :int(game_state.base_y)], opt.image_size, opt.image_size)image = torch.from_numpy(image)if torch.cuda.is_available():model.cuda()image = image.cuda()state = torch.cat(tuple(image for _ in range(4)))[None, :, :, :]replay_memory = []iter = 0while iter < opt.num_iters:prediction = model(state)[0]# Exploration or exploitationepsilon = opt.final_epsilon + ((opt.num_iters - iter) * (opt.initial_epsilon - opt.final_epsilon) / opt.num_iters)u = random()random_action = u <= epsilonif random_action:print("Perform a random action")action = randint(0, 1)else:action = torch.argmax(prediction)next_image, reward, terminal = game_state.next_frame(action)next_image = pre_processing(next_image[:game_state.screen_width, :int(game_state.base_y)], opt.image_size,opt.image_size)next_image = torch.from_numpy(next_image)if torch.cuda.is_available():next_image = next_image.cuda()next_state = torch.cat((state[0, 1:, :, :], next_image))[None, :, :, :]replay_memory.append([state, action, reward, next_state, terminal])if len(replay_memory) > opt.replay_memory_size:del replay_memory[0]batch = sample(replay_memory, min(len(replay_memory), opt.batch_size))state_batch, action_batch, reward_batch, next_state_batch, terminal_batch = zip(*batch)state_batch = torch.cat(tuple(state for state in state_batch))action_batch = torch.from_numpy(np.array([[1, 0] if action == 0 else [0, 1] for action in action_batch], dtype=np.float32))reward_batch = torch.from_numpy(np.array(reward_batch, dtype=np.float32)[:, None])next_state_batch = torch.cat(tuple(state for state in next_state_batch))if torch.cuda.is_available():state_batch = state_batch.cuda()action_batch = action_batch.cuda()reward_batch = reward_batch.cuda()next_state_batch = next_state_batch.cuda()current_prediction_batch = model(state_batch)next_prediction_batch = model(next_state_batch)y_batch = torch.cat(tuple(reward if terminal else reward + opt.gamma * torch.max(prediction) for reward, terminal, prediction inzip(reward_batch, terminal_batch, next_prediction_batch)))q_value = torch.sum(current_prediction_batch * action_batch, dim=1)optimizer.zero_grad()# y_batch = y_batch.detach()loss = criterion(q_value, y_batch)loss.backward()optimizer.step()state = next_stateiter += 1print("Iteration: {}/{}, Action: {}, Loss: {}, Epsilon {}, Reward: {}, Q-value: {}".format(iter + 1,opt.num_iters,action,loss,epsilon, reward, torch.max(prediction)))writer.add_scalar('Train/Loss', loss, iter)writer.add_scalar('Train/Epsilon', epsilon, iter)writer.add_scalar('Train/Reward', reward, iter)writer.add_scalar('Train/Q-value', torch.max(prediction), iter)if (iter+1) % 1000000 == 0:torch.save(model, "{}/flappy_bird_{}".format(opt.saved_path, iter+1))torch.save(model, "{}/flappy_bird".format(opt.saved_path))if __name__ == "__main__":opt = get_args()train(opt)

相关文章:

用深度强化学习来玩Flappy Bird

目录 演示视频 核心代码 演示视频 用深度强化学习来玩Flappy Bird 核心代码 import torch.nn as nnclass DeepQNetwork(nn.Module):def __init__(self):super(DeepQNetwork, self).__init__()self.conv1 nn.Sequential(nn.Conv2d(4, 32, kernel_size8, stride4), nn.ReLU(inp…...

HTML5-4-表单

文章目录 表单属性表单标签输入元素文本域&#xff08;Text Fields&#xff09;密码字段单选按钮&#xff08;Radio Buttons&#xff09;复选框&#xff08;Checkboxes&#xff09;按钮&#xff08;button&#xff09;提交按钮(Submit)label标签 文本框&#xff08;textarea&am…...

Nacos 开源版的使用测评

文章目录 一、Nacos的使用二、Nacos和Eureka在性能、功能、控制台体验、上下游生态和社区体验的对比&#xff1a;三、记使使用Nacos中容易犯的错误四、对Nacos开源提出的一些需求 一、Nacos的使用 这里配置mysql的连接方式&#xff0c;spring.datasource.platformmysql是老版本…...

【Linux】一些常见查看各种各样信息的命令

Linux命令 find命令&#xff0c;用来查找文件。常用的按照名字查找-name&#xff0c;按照文件类型查找-type&#xff0c;linux常用的文件类型有七种&#xff0c;普通文件&#xff0c;目录文件&#xff0c;管道&#xff0c;套接字&#xff0c;软链接&#xff0c;块设备&#xf…...

51单片机DHT11温湿度控制系统仿真设计( proteus仿真+程序+原理图+报告+讲解视频)

51单片机DHT11温湿度控制系统仿真设计 1.主要功能&#xff1a;2.仿真3. 程序代码4. 原理图元器件清单5. 设计报告6. 设计资料内容清单&下载链接 51单片机DHT11温湿度控制系统仿真设计( proteus仿真程序原理图报告讲解视频&#xff09; 仿真图proteus8.9及以上 程序编译器&…...

神仙级python入门教程(非常详细),从0到精通,从看这篇开始!

毫无疑问&#xff0c;Python 是当下最火的编程语言之一。对于许多未曾涉足计算机编程的领域「小白」来说&#xff0c;深入地掌握 Python 看似是一件十分困难的事。其实&#xff0c;只要掌握了科学的学习方法并制定了合理的学习计划&#xff0c;Python 从 入门到精通只需要一个月…...

详解4种类型的爬虫技术

聚焦网络爬虫是“面向特定主题需求”的一种爬虫程序&#xff0c;而通用网络爬虫则是捜索引擎抓取系统&#xff08;Baidu、Google、Yahoo等&#xff09;的重要组成部分&#xff0c;主要目的是将互联网上的网页下载到本地&#xff0c;形成一个互联网内容的镜像备份。 增量抓取意…...

QTday1基础

作业 一、做个QT页面 #include "hqyj.h"HQYJ::HQYJ(QWidget *parent)//构造函数定义: QWidget(parent)//显性调用父类的有参构造 {//主界面设置this->resize(540,410);//设置大小this->setFixedSize(540,410);//设置固定大小this->setWindowIcon(QIcon(&q…...

activiti 通过xml上传 直接部署模型

通过流程xml 直接先发布模型&#xff0c;然后再通过发布模型之后的流程定义获取bpmn model来创建Model. 1、通过xml先发布模型 InputStream bpmnStream multipartFile.getInputStream() deployment repositoryService.createDeployment().addInputStream(multipartFile.getO…...

算法题打卡day56-编辑距离 | 583. 两个字符串的删除操作、72. 编辑距离

583. 两个字符串的删除操作 - 力扣&#xff08;LeetCode&#xff09; 状态&#xff1a;查看思路后AC。 和查找子序列的操作类似&#xff0c;但是考虑的是删除操作。代码如下&#xff1a; class Solution { public:int minDistance(string word1, string word2) {int len1 wor…...

SQL中的CASE WHEN语句:从基础到高级应用指南

SQL中的CASE WHEN语句&#xff1a;从基础到高级应用指南 准备工作 - 表1: products 示例数据&#xff1a; 我们使用一个名为"Products"的表&#xff0c;包含以下列&#xff1a;ProductID、ProductName、CategoryID、UnitPrice、StockQuantity。 -- 建表 CREATE TA…...

超时取消子线程任务

文章目录 前言一、编码思路二、使用步骤直接上代码 总结 前言 问题背景: 主线程需要执行一些任务,不能影响主任务执行,这些任务有超时时间,当超过处理时间后,应该不予处理;如果未超时,应该获取到这些任务的执行结果; 一、编码思路 由于主线程正常执行不能影响,任务会处理很久…...

模块化---common.js

入口文件&#xff1a;app.js // require是同步加载 // 客户端&#xff1a;common.js的模块化&#xff0c;需要browserify编译之后才能使用 // 服务端&#xff1a;运行时同步加载&#xff0c;无问题 let module1 require(./module1.js) let module2 require(./module2.js) co…...

VSCode下载、安装及配置、调试的一些过程理解

第一步先下载了vscode&#xff0c;官方地址为&#xff1a;https://code.visualstudio.com/Download 第二步安装vscode&#xff0c;安装环境是win10&#xff0c;安装基本上就是一步步默认即可。 第三步汉化vscode&#xff0c;这一步就是去扩展插件里面下载一个中文插件即可&am…...

KC705开发板——MGT IBERT测试记录

本文介绍使用KC705开发板进行MGT的IBERT测试。 KC705开发板 KC705开发板的图片如下图所示。FPGA芯片型号为XC7K325T-2FFG900C。 MGT MGT是 Multi-Gigabit Transceiver的缩写&#xff0c;是Multi-Gigabit Serializer/Deserializer (SERDES)的别称。MGT包含GTP、GTX、GTH、G…...

前端代码优化散记

把import Button from xxx 的引入方式&#xff0c;变成import {Button} from xxx 的方式引入&#xff0c;以利于按需打包。原生监听事件、定时器等&#xff0c;必须在componentWillUnmount中清除&#xff0c;大型项目会发生内存泄露&#xff0c;极度影响性能。使用PureComponen…...

HTML <map> 标签的使用

map标签的用途&#xff1a;是与img标签绑定使用的&#xff0c;常被用来赋予给客户端图像某处区域特殊的含义&#xff0c;点击该区域可跳转到新的文档。 编写格式&#xff1a; <img src"图片" border"0" usemap"#planetmap" alt"Planets…...

stable diffusion实践操作-大模型介绍

本文专门开一节写大模型相关的内容&#xff0c;在看之前&#xff0c;可以同步关注&#xff1a; stable diffusion实践操作 模型下载网站 国内的是&#xff1a;https://www.liblibai.com 国外的是&#xff1a;https://civitai.com&#xff08;科学上网&#xff09; 一、发展历…...

W5500-EVB-PICO进行MQTT连接订阅发布教程(十二)

前言 上一章我们用开发板通过SNTP协议获取网络协议&#xff0c;本章我们介绍一下开发板通过配置MQTT连接到服务器上&#xff0c;并且订阅和发布消息。 什么是MQTT&#xff1f; MQTT是一种轻量级的消息传输协议&#xff0c;旨在物联网&#xff08;IoT&#xff09;应用中实现设备…...

90、00后严选出的数据可视化工具:奥威BI工具

90、00后主打一个巧用工具&#xff0c;绝不低效率上班&#xff0c;因此当擅长大数据智能可视化分析的BI数据可视化工具出现后&#xff0c;自然而然地就成了90、00后职场人常用的数据可视化工具。 奥威BI工具三大特点&#xff0c;让职场人眼前一亮&#xff01; 1、零编程&…...

别再乱用npm install了!手把手教你用npx only-allow为项目指定包管理器(支持pnpm/yarn/npm)

用only-allow统一团队包管理器&#xff1a;从配置到CI的全流程指南 你是否曾经在拉取一个新项目后&#xff0c;面对npm install、yarn还是pnpm i的抉择感到困惑&#xff1f;或者更糟的是&#xff0c;团队成员混用不同包管理器导致node_modules结构不一致&#xff0c;引发各种诡…...

QMCDecode终极指南:3步解锁QQ音乐加密格式,实现跨平台音乐自由

QMCDecode终极指南&#xff1a;3步解锁QQ音乐加密格式&#xff0c;实现跨平台音乐自由 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac&#xff0c;qmc0,qmc3转mp3, mflac,mflac0等转flac)&#xff0c;仅支持macOS&#xff0c;可自动识别到QQ音乐下载目…...

Lindy自动化效率翻倍的秘密:从零搭建高可靠多步骤任务流的7步黄金流程

更多请点击&#xff1a; https://intelliparadigm.com 第一章&#xff1a;Lindy自动化效率翻倍的秘密&#xff1a;从零搭建高可靠多步骤任务流的7步黄金流程 Lindy自动化平台以“越久越可靠”为设计哲学&#xff0c;将经典软件工程原则与现代可观测性实践深度融合。其核心优势…...

荣耀出征官方网站下载正版手游 翅膀养成细节玩法全方位讲解

玩荣耀出征的玩家都清楚&#xff0c;翅膀不仅是角色的颜值象征&#xff0c;更是提升整体战力的核心途径。很多新手玩家只顾着升级、刷装备&#xff0c;完全忽略翅膀养成&#xff0c;导致等级很高但战力始终上不去。还有不少玩家胡乱合成、盲目进阶&#xff0c;浪费了大量稀有翅…...

神经网络与深度学习 第3周课程总结

深度学习视觉应用课程总结 一、常用计算机视觉数据集数据集名称发布方/年份规模图像规格类别数主要用途核心特点MNIST美国国家标准与技术研究院60k训练10k测试2828灰度图10类(0-9手写数字)入门级图像分类最经典的手写数字识别基准数据集Fashion-MNISTZalando(2017)60k训练10k测…...

Burp Suite拦截与替换机制深度解析:从协议层到规则链

1. 这不是“点开就能用”的功能&#xff0c;而是你和目标系统之间的一道可编程闸门很多人第一次在Burp Suite里点开Proxy → Intercept&#xff0c;看到HTTP请求被拦下来&#xff0c;兴奋地改个User-Agent、删个Cookie就点Forward&#xff0c;以为自己已经掌握了“拦截与替换”…...

利用 Taotoken 多模型能力为智能客服场景提供备份路由

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 利用 Taotoken 多模型能力为智能客服场景提供备份路由 智能客服系统是许多企业与用户交互的关键入口&#xff0c;其响应能力和服务…...

机器学习力场攻克Peierls相变动力学:从对称性描述符到畴生长标度律

1. 项目概述&#xff1a;当机器学习遇见Peierls相变在凝聚态物理和材料科学的前沿&#xff0c;我们常常被一个核心问题所困扰&#xff1a;如何精确地模拟那些由电子和晶格&#xff08;原子&#xff09;强烈耦合所驱动的复杂动力学过程&#xff1f;这类系统&#xff0c;比如电荷…...

Windows Cleaner如何5步解决C盘爆红问题?完全指南助你释放宝贵空间

Windows Cleaner如何5步解决C盘爆红问题&#xff1f;完全指南助你释放宝贵空间 【免费下载链接】WindowsCleaner Windows Cleaner——专治C盘爆红及各种不服&#xff01; 项目地址: https://gitcode.com/gh_mirrors/wi/WindowsCleaner 你是否曾经面对C盘爆红的警告束手无…...

终极虚拟显示器解决方案:ParsecVDisplay完整使用指南

终极虚拟显示器解决方案&#xff1a;ParsecVDisplay完整使用指南 【免费下载链接】parsec-vdd ✨ Perfect virtual display for game streaming 项目地址: https://gitcode.com/gh_mirrors/pa/parsec-vdd ParsecVDisplay是一个基于Parsec虚拟显示驱动(VDD)的独立应用程序…...