Putting together all of the pieces of a full stack JavaScript application can be a complex endeavor.
将全栈JavaScript应用程序的所有部分放在一起可能是一项复杂的工作。
In this tutorial, we're going to build a multiplayer tabletop game simulator using Vue, Phaser, Node/Express, and Socket.IO to learn several concepts that will be useful in any full stack app.
在本教程中,我们将使用Vue , Phaser , Node / Express和Socket.IO构建一个多人桌面游戏模拟器,以学习在任何全栈应用程序中都有用的几个概念。
You can follow along with this video tutorial as well (1 hour 16 minute watch):
您也可以按照此视频教程进行操作(观看1小时16分钟):
All of the project files for this tutorial are available on GitHub.
Our project will feature a Phaser game instance that will allow us to create tokens and cards on screen, and move them around on a digital game board.
我们的项目将包含一个Phaser游戏实例,该实例将使我们能够在屏幕上创建令牌和卡,并在数字游戏板上移动它们。
The Phaser instance will be wrapped in a Vue component that will handle things like multiplayer chat and commands. Together, Phaser and Vue will comprise our front end (referred to from here on as the "client"), and we'll use Socket.IO to communicate with other players and tie together the front and back ends of our app.
Phaser实例将包装在Vue组件中,该组件将处理多人聊天和命令之类的事情。 Phaser和Vue共同构成了我们的前端(从此处称为“客户端”),并且我们将使用Socket.IO与其他播放器进行通信,并将应用程序的前端和后端捆绑在一起。
The back end (referred to from here on as the "server") will be a simple Express server that receives Socket.IO events from the client and acts accordingly. The whole application will run on Node as its runtime.
后端(从此处称为“服务器”)将是一个简单的Express服务器,该服务器从客户端接收Socket.IO事件并采取相应的措施。 整个应用程序将在运行时在Node上运行。
You don't need to be an expert in any of the above frameworks to complete this project, but it would be a good idea to have a solid foundation in basic JavaScript and HTML/CSS before trying to tackle the specifics. You can also follow along with my series on Learning JavaScript by Making Digital Tabletop Games and Web Apps.
您无需成为上述任何一个框架的专家即可完成此项目,但是在尝试解决具体问题之前,在基本JavaScript和HTML / CSS方面拥有坚实的基础是一个好主意。 您还可以按照我的系列文章“ 通过制作数字桌面游戏和Web应用程序学习JavaScript ”进行学习 。
You'll also want to make sure that you have Node and Git installed, along with your favorite code editor and a command line interface (you can follow my tutorial on setting up an IDE here if you need help).
您还需要确保已安装Node和Git ,以及您喜欢的代码编辑器和命令行界面(如果需要帮助,可以按照我的教程在这里设置IDE)。
Let's get started!
让我们开始吧!
We'll begin building our client by installing the Vue CLI, which will help us with some tooling and allow us to make changes to our files without having to reload our web browser.
我们将通过安装Vue CLI开始构建客户端,这将帮助我们提供一些工具,并使我们无需重新加载Web浏览器即可对文件进行更改。
In a command line, type in the following to install the Vue CLI globally:
在命令行中,键入以下内容以全局安装Vue CLI:
npm install -g @vue/cli
Navigate to a desired directory and create a new folder for our project:
导航到所需目录并为我们的项目创建一个新文件夹:
mkdir tabletop-project
cd tabletop-project
Now we can use the Vue CLI to template a front end project for us:
现在,我们可以使用Vue CLI为我们模板化前端项目:
vue create client
You can just hit "enter" at the ensuing prompts unless you have specific preferences.
除非您有特定的首选项,否则您只需在随后的提示中单击“输入”即可。
The Vue CLI has helpfully templated a front end project for us, which we can view in our code editor:
Vue CLI为我们有用地模板化了一个前端项目,我们可以在代码编辑器中查看该项目:
Let's navigate to our new client folder in our CLI and run the template app:
让我们导航到CLI中的新客户端文件夹并运行模板应用程序:
cd client
npm run serve
After a little work, the Vue CLI should begin displaying our app in a web browser at the default http://localhost:8080:
经过一些工作之后,Vue CLI应该开始在默认的http:// localhost:8080的网络浏览器中显示我们的应用程序:
Cool! We have the basic structure of our client. Let's break it by creating two new components in the /components folder, called Game.vue and Chat.vue (you can go ahead and delete HelloWorld.vue and anything in the assets folder if you're obsessed with tidiness like I am).
凉! 我们拥有客户的基本结构。 让我们通过在/ components文件夹中创建两个新组件,即Game.vue和Chat.vue来打破它(您可以继续删除HelloWorld.vue以及Assets文件夹中的任何内容,如果您对像我这样的整洁很着迷)。
Replace the code in App.vue with the following:
用以下代码替换App.vue中的代码:
<template>
<div id="app">
<div id="game">
<Game />
</div>
<div id="border" />
<div id="input">
<Chat />
</div>
</div>
</template>
<script>
import Chat from './components/Chat.vue';
import Game from './components/Game.vue';
export default {
name: 'App',
components: {
Chat,
Game
}
}
</script>
<style>
#app {
font-family: 'Trebuchet MS';
text-align: left;
background-color: black;
color: cyan;
display: flex;
}
#game {
width: 50vw;
height: 100vh;
}
#input {
width: 50vw;
height: 100vh;
}
#border {
border-right: 2px solid cyan;
}
@media (max-width: 1000px) {
#app {
flex-direction: column;
}
#game {
width: 100vw;
height: 50vh;
}
#input {
width: 100vw;
height: 50vh;
}
}
</style>
As you can see, a Vue component ordinarily has three sections: Template, Script, and Style, which contain any HTML, JavaScript, and CSS for that component, respectively. We've just imported our Game and Chat components here and added a little styling to give it a cyberpunk feel when it's all up and running.
如您所见,Vue组件通常具有三个部分:Template,Script和Style,分别包含该组件的任何HTML,JavaScript和CSS。 我们刚刚在这里导入了Game和Chat组件,并添加了一些样式,使其在启动和运行时都具有赛博朋克的感觉。
That's actually all that we need to do to set up our App.vue component, which will house everything else in our client. Before we can actually do anything with it, we'll need to get our server working!
实际上,这就是设置App.vue组件所需的全部工作,该组件将在客户端中容纳所有其他内容。 在我们对其真正执行任何操作之前,我们需要使服务器工作!
At our root directory (tabletop-project, above /client), initialize a new project in a new command line interface by typing:
在我们的根目录(// client上方的tabletop-project)中,通过键入以下内容在新的命令行界面中初始化新项目:
npm init
Like with our client, you can go ahead and press "enter" at the prompts unless there are specifics that you'd like to designate at this time.
与我们的客户一样,您可以继续操作,并在提示时按“输入”,除非您此时不想指定具体细节。
We'll need to install Express and Socket.IO, along with Nodemon to watch our server files for us and reboot as necessary:
我们将需要安装Express和Socket.IO以及Nodemon,以便为我们监视服务器文件并根据需要重新启动:
npm install --save express socket.io nodemon
Let's open up the new package.json file in that root directory and add a "start" command in the "scripts" section:
让我们在该根目录中打开新的package.json文件,并在“脚本”部分中添加“开始”命令:
"scripts": {
"start": "nodemon server.js"
},
Create a new file called server.js in this directory, and enter the following code:
在此目录中创建一个名为server.js的新文件,然后输入以下代码:
const server = require('express')();
const http = require('http').createServer(server);
const io = require('socket.io')(http);
io.on('connection', function (socket) {
console.log('A user connected: ' + socket.id);
socket.on('send', function (text) {
let newText = "<" + socket.id + "> " + text;
io.emit('receive', newText);
});
socket.on('disconnect', function () {
console.log('A user disconnected: ' + socket.id);
});
});
http.listen(3000, function () {
console.log('Server started!');
});
Excellent! Our simple server will now listen at http://localhost:3000, and use Socket.IO to log to the console when a user connects and disconnects, with their socket ID.
优秀的! 现在,我们的简单服务器将在http:// localhost:3000进行侦听,并在用户使用其套接字ID进行连接和断开连接时使用Socket.IO登录到控制台。
When the server receives a "send" event from a client, it will create a new text string that includes the socket ID of the client that emitted the event, and emit its own "receive" event to all clients with the text that it received, interpolated with the socket ID.
当服务器从客户端接收到“发送”事件时,它将创建一个新的文本字符串,其中包括发出该事件的客户端的套接字ID,并使用接收到的文本向所有客户端发出其自己的“ receive”事件,内插套接字ID。
We can test the server by returning to our command line and starting it up :
我们可以通过返回命令行并启动来测试服务器:
npm run start
The command console should now display:
现在,命令控制台应显示:
Cool! Let's return to the Chat component of our client to start building out our front end functionality.
凉! 让我们回到客户端的Chat组件开始构建前端功能。
Let's open a separate command line interface and navigate to the /client directory. Within that directory, install the client version of Socket.IO:
让我们打开一个单独的命令行界面,然后导航到/ client目录。 在该目录中,安装Socket.IO的客户端版本:
npm install --save socket.io-client
In /client/src/components/Chat.vue, add the following code:
在/client/src/components/Chat.vue中,添加以下代码:
<template>
<div id="container">
<div id="output">
<h1>STRUCT</h1>
<p v-for="(text, index) in textOutput" :key="index">{{text}}</p>
</div>
<div id="input">
<form>
<input type="text" v-model="textInput" :placeholder="textInput" />
<input type="submit" value="Send" v-on:click="submitText" />
</form>
</div>
</div>
</template>
<script>
import io from 'socket.io-client';
let socket = io('http://localhost:3000');
export default {
name: 'Chat',
data: function () {
return {
textInput: null,
textOutput: []
}
},
methods: {
submitText: function (event) {
event.preventDefault();
socket.emit('send', this.textInput);
}
},
created: function () {
socket.on('connect', () => {
console.log('Connected!');
});
socket.on('receive', (text) => {
this.textOutput.push(text);
this.textInput = null;
});
}
}
</script>
<style scoped>
#container {
text-align: left;
display: flex;
flex-direction: column;
margin-left: 1vw;
min-height: 100vh;
}
h1 {
text-align: center;
}
.hotpink {
color: hotpink;
}
#input {
position: fixed;
margin-top: 95vh;
}
input[type=text] {
height: 20px;
width: 40vw;
border: 2px solid cyan;
background-color: black;
color: hotpink;
padding-left: 1em;
}
input[type=submit]{
height: 25px;
width: 5vw;
background-color: black;
color: cyan;
border: 2px solid cyan;
margin-right: 2vw;
}
input[type=submit]:focus{
outline: none;
}
input[type=submit]:hover{
color: hotpink;
}
@media (max-width: 1000px) {
#container {
border-left: none;
border-top: 2px solid cyan;
min-height: 50vh;
}
#input {
margin-top: 43vh;
}
#output {
margin-right: 10vw;
}
input[type=text] {
width: 60vw;
}
input[type=submit] {
min-width: 10vw;
}
}
</style>
Let's examine the above from bottom to top before moving forward. Between the <style> tags, we've added some CSS to punch-up the cyberpunkiness of our chat. You can style this however you'd like!
在继续之前,让我们从下至上研究以上内容。 在<style>标记之间,我们添加了一些CSS来增强聊天的网络朋克感。 您可以根据需要设置样式!
Between the <script> tags, we've imported the client version of Socket.IO and stored it in a variable called "socket" that communicates through http://localhost:3000, where the server is listening.
在<script>标记之间,我们导入了Socket.IO的客户端版本,并将其存储在名为“ socket”的变量中,该变量通过服务器正在侦听的http:// localhost:3000进行通信。
We've then given the component a name ("Chat"), and utilized the data, methods, and created objects that Vue uses to handle interactivity for us.
然后,我们为该组件命名(“聊天”),并利用Vue用来为我们处理交互性的数据,方法和创建的对象。
In the data object, we store two variables: textInput, which starts out as null, and textOutput, which is an empty array.
在数据对象中,我们存储了两个变量:textInput(其开始为null)和textOutput(其为空数组)。
In the methods object, we create a simple function, submitText, that emits a "send" event through Socket.IO along with the textInput and prevents the default behavior of such an event (such as sending data through an HTML form).
在方法对象中,我们创建一个简单的函数SubmitText,该函数通过Socket.IO与textInput一起发出“发送”事件,并防止此类事件的默认行为(例如,通过HTML表单发送数据)。
In the created object, which is triggered when the component is initialized, we have two references to the socket. The first indicates that when it receives a "connect" event from the server, the socket should log to the console that it has "Connected!" The second indicates that when the socket receives a "receive" event, it should push the text from that event to the textOutput array and clear the textInput variable.
在初始化组件时触发的创建对象中,我们有两个对套接字的引用。 第一个指示,当它从服务器接收到“连接”事件时,套接字应登录到其具有“已连接!”的控制台。 第二个参数指示套接字在接收到“ receive”事件时,应将事件中的文本推送到textOutput数组并清除textInput变量。
Between our <template> tags, we have our HTML that includes input and output sections. The output section has a header named "Struct" (which is the programming language in my books and games), and utilizes Vue's list rendering to display a <p> element for each piece of text in the textOutput array.
在<template>标记之间,我们有包含输入和输出部分HTML。 输出部分有一个名为“ Struct”的头(这是我的书和游戏中的编程语言),并利用Vue的列表渲染为textOutput数组中的每个文本显示<p>元素。
The input section has a simple form with Vue form input bindings and an event handler to receive text input, store it in our textInput variable, and trigger the "send" Socket.IO event when the "Send" button is clicked.
输入部分具有一个带有Vue 表单输入绑定的简单表单,以及一个事件处理程序,用于接收文本输入,将其存储在我们的textInput变量中,并在单击“发送”按钮时触发“发送” Socket.IO事件。
Phew! Our chat is now functional. Save everything and navigate to your browser tab that is running the client at http://localhost:8080:
! 现在,我们的聊天功能正常了。 保存所有内容,并浏览至运行客户端的浏览器选项卡,该选项卡位于http:// localhost:8080:
Note that you can open up another browser tab, which will connect to the server with a new socket ID, and the chat should begin populating among both clients:
请注意,您可以打开另一个浏览器选项卡,该选项卡将使用新的套接字ID连接到服务器,并且聊天应开始在两个客户端之间进行:
Meanwhile, your command line console should also be indicating when clients connect to and disconnect from the server (with different socket IDs, of course):
同时,您的命令行控制台还应指示客户端何时连接到服务器或从服务器断开连接(当然使用不同的套接字ID):
Awesome. Let's move to building our tabletop simulator in Phaser!
太棒了 让我们开始在Phaser中构建桌面模拟器!
We'll need a Vue component to house our Phaser instance, and to do so, we'll borrow from Sun0fABeach's Vue - Phaser 3 Webpack boilerplate (you could probably even use this boilerplate to create your client if you so desired).
我们需要一个Vue组件来容纳我们的Phaser实例,为此,我们将从Sun0fABeach的Vue-Phaser 3 Webpack样板中借用(如果您愿意,甚至可以使用此样板来创建您的客户端)。
In our /client/src/components/Game.vue file, add the following code:
在我们的/client/src/components/Game.vue文件中,添加以下代码:
<template>
<div :id="containerId" v-if="downloaded" />
<div class="placeholder" v-else>
Downloading...
</div>
</template>
<script>
export default {
name: 'Game',
data: function () {
return {
downloaded: false,
gameInstance: null,
containerId: 'game-container'
}
},
async mounted() {
const game = await import(../game/game');
this.downloaded = true;
this.$nextTick(() => {
this.gameInstance = game.launch(this.containerId)
})
},
destroyed() {
this.gameInstance.destroy(false);
}
}
</script>
<style scoped>
</style>
This component will render our game instance when it's ready, and keep a placeholder until that time (usually just a few seconds). It won't work just yet, as we haven't created a game instance with which to work.
该组件将在准备好游戏实例后呈现它,并保留一个占位符直到该时间(通常只有几秒钟)。 由于我们尚未创建可用于工作的游戏实例,因此它尚无法正常工作。
In a command line interface at the /client directory, type the following
在/ client目录的命令行界面中,键入以下内容
npm install --save phaser
Phaser will handle the rendering all of our game objects like tokens and cards, while also making them interactive with drag-and-drop functionality.
Phaser将处理渲染我们所有游戏对象(如令牌和卡)的过程,同时还使其具有拖放功能。
Within the /client/src folder, add a new folder called "game", and a subfolder within that folder called "scenes".
在/ client / src文件夹中,添加一个名为“ game”的新文件夹,并在该文件夹中添加一个名为“ scenes”的子文件夹。
Within the /client/src/game folder, add a file called game.js, and within /client/src/game/scenes, add a file called gamescene.js. Your file structure should now look like:
在/ client / src / game文件夹中,添加一个名为game.js的文件,在/ client / src / game / scenes中,添加一个名为gamecene.js的文件。 您的文件结构现在应如下所示:
Our game.js file will handle the initial setup for our Phaser instance, importing our gamescene.js and launching our game into the containerId of our Vue component (it also scales the instance to the size of the container). Here's what it should look like:
我们的game.js文件将处理Phaser实例的初始设置,导入gamecene.js并将我们的游戏启动到Vue组件的containerId中(它还将实例缩放到容器的大小)。 它应该是这样的:
import Phaser from "phaser";
import GameScene from "./scenes/gamescene";
function launch(containerId) {
return new Phaser.Game({
type: Phaser.AUTO,
parent: containerId,
scene: [
GameScene
],
scale: {
mode: Phaser.Scale.FIT,
width: '100%',
height: '100%'
}
});
}
export default launch;
export { launch }
The main functionality of our simulator will be in the gamescene.js file, where we'll write:
我们模拟器的主要功能将在gamecene.js文件中,我们将在其中编写:
import Phaser from 'phaser';
import io from 'socket.io-client';
export default class GameScene extends Phaser.Scene {
constructor() {
super({
key: 'GameScene'
});
}
preload() {
}
create() {
this.socket = io('http://localhost:3000');
this.socket.on('struct create', (width, height) => {
let token = this.add.rectangle(300, 300, width, height, 0x00ffff).setInteractive();
this.input.setDraggable(token);
});
this.input.on('drag', (pointer, gameObject, dragX, dragY) => {
gameObject.x = dragX;
gameObject.y = dragY;
});
}
update() {
}
}
Our Phaser architecture utilizes JavaScript classes to create scenes, and has three main functions: preload, create, and update.
我们的Phaser架构利用JavaScript 类创建场景 ,并具有三个主要功能:预加载,创建和更新。
The preload function is used for prepping assets like sprites for use within a scene.
预加载功能用于准备在场景中使用的诸如精灵之类的资产。
The update function is called once per frame, and we're not making use of it in our project.
每帧调用一次update函数,我们在项目中没有使用它。
The create function is called when a scene is created, and where all of our work is being done here. We initialize a socket variable and store the Socket.IO connection at http://localhost:3000 within it, then reference a "struct create" event that we expect to receive from the server.
创建场景时以及在此处完成所有工作的地方将调用create函数。 我们初始化一个套接字变量,并将Socket.IO连接存储在其中的http:// localhost:3000,然后引用一个我们希望从服务器接收到的“结构创建”事件。
When the client receives a "struct create" event, our Phaser instance should create a rectangle at the (x, y) coordinates of (300, 300), using the width and height parameters that are designated by that event, and a fun cyberpunk color that we've chosen. Phaser will then set that rectangle to be interactive, and alert the input system that it should also be draggable.
当客户端收到“结构创建”事件时,我们的Phaser实例应使用该事件指定的width和height参数以及有趣的Cyberpunk在(x,y)坐标(300,300)处创建一个矩形我们选择的颜色。 然后,Phaser将该矩形设置为交互式,并警告输入系统它也应该可拖动。
We've also written a little bit of logic that tells Phaser what it should do when the rectangle is dragged; namely, it should follow the direction of the mouse pointer.
我们还编写了一些逻辑,以告诉Phaser拖动矩形时应执行的操作。 也就是说,它应遵循鼠标指针的方向。
All we have to do now is to jump back into our server.js and add logic for our "struct create" event:
现在我们要做的就是跳回到server.js并为“ struct create”事件添加逻辑:
const server = require('express')();
const http = require('http').createServer(server);
const io = require('socket.io')(http);
io.on('connection', function (socket) {
console.log('A user connected: ' + socket.id);
socket.on('send', function (text) {
let newText = "<" + socket.id + "> " + text;
if (text === 'struct card') {
io.emit('struct create', 130, 180)
};
if (text === 'struct token') {
io.emit('struct create', 100, 100)
};
io.emit('receive', newText);
});
socket.on('disconnect', function () {
console.log('A user disconnected: ' + socket.id);
});
});
http.listen(3000, function () {
console.log('Server started!');
});
Our server now acts as a simple parser when it receives a "send" event from a client. If the client sends the text "struct card", the server will emit our "struct create" event, with arguments of 130 x 180 pixels for the width and height of a card.
现在,当服务器从客户端接收到“发送”事件时,它就充当了简单的解析器。 如果客户端发送文本“结构卡”,则服务器将发出“结构创建”事件,其中卡的宽度和高度的参数为130 x 180像素。
If the client sends the text "struct token", the server will instead emit our "struct create" event with arguments of 100 x 100 pixels for the width and height of a token.
如果客户端发送文本“结构令牌”,则服务器将发出带有令牌宽度和高度100 x 100像素的参数的“结构创建”事件。
Try it! Save everything, make sure the server is running, and open a couple of tabs in a web browser pointed to http://localhost:8080. When you chat in one tab, it should appear in the other with your client's socket ID, and vice versa.
试试吧! 保存所有内容,确保服务器正在运行,然后在指向http:// localhost:8080的Web浏览器中打开几个选项卡。 在一个选项卡中聊天时,该选项卡应与客户端的套接字ID一起显示在另一个选项卡中,反之亦然。
If your chat is the command "struct card" or "struct token", a draggable card or token, respectively, should appear in both clients.
如果您的聊天是命令“结构卡”或“结构令牌”,则两个客户端中应分别出现可拖动的卡或令牌。
Neat!
整齐!
By following this tutorial, you should now have a working multiplayer tabletop game simulator with chat, card and token creation, and drag-and-drop functionality.
通过遵循本教程,您现在应该拥有一个可以正常工作的多人桌面游戏模拟器,该模拟器具有聊天,创建卡片和令牌以及拖放功能。
You can continue to build on this simple full stack app by enhancing the styling, adding a scroll bar to the chat, or allowing players to chose usernames and log into specific chat instances by using Socket.IO rooms.
您可以通过增强样式,向聊天添加滚动条或允许玩家选择用户名并使用Socket.IO聊天室登录特定的聊天实例,来继续在此简单的全栈应用程序上进行构建。
You can improve on the board game functionality by dealing multiple cards and tokens at once, or getting familiar with the Phaser examples to add your own features. You can also follow along with my tutorial on How to Build a Multiplayer Card Game with Phaser 3, Express, and Socket.IO for inspiration.
您可以通过一次处理多个卡牌和令牌来改善棋盘游戏的功能,或者熟悉Phaser示例来添加自己的功能。 您也可以跟随我的教程“ 如何使用Phaser 3,Express和Socket.IO构建多人纸牌游戏”获得灵感。
Of course, there's no reason that you'd need to use chat commands to create game objects. You could do all of that from within the Phaser instance if you'd like, but you'll need to create your own buttons or some other input interactivity (in my experience, Vue is far better at handling text, hence our chat commands).
当然,没有理由不需要使用聊天命令来创建游戏对象。 您可以根据需要在Phaser实例中进行所有操作,但是您需要创建自己的按钮或其他一些输入交互功能(根据我的经验,Vue在处理文本方面要好得多,因此我们的聊天命令就更好了) 。
The current functionality, could, however, be useful in the case that you'd want to be able to render, say, dice on screen by running a chat command.
但是,如果您希望能够通过运行聊天命令在屏幕上呈现骰子,则当前功能可能会很有用。
Additionally, if you'd like to deploy your new app, you can first read my article on Three Things to Consider Before Deploying Your First Full Stack App, then follow along with my tutorial to Learn How to Deploy a Full Stack Web App with Heroku.
此外,如果要部署新应用程序,则可以先阅读我的文章“ 在部署第一个完整堆栈应用程序之前要考虑的三件事” ,然后按照我的教程学习如何使用Heroku部署完整堆栈Web应用程序。 。
Happy coding!
编码愉快!
If you enjoyed this article, please consider checking out my games and books, subscribing to my YouTube channel, or joining the Entromancy Discord.
如果您喜欢这篇文章,请考虑查看我的游戏和书籍 , 订阅我的YouTube频道或加入Entromancy Discord 。
M. S. Farzan, Ph.D. has written and worked for high-profile video game companies and editorial websites such as Electronic Arts, Perfect World Entertainment, Modus Games, and MMORPG.com, and has served as the Community Manager for games like Dungeons & Dragons Neverwinter and Mass Effect: Andromeda. He is the Creative Director and Lead Game Designer of Entromancy: A Cyberpunk Fantasy RPG and author of The Nightpath Trilogy. Find M. S. Farzan on Twitter @sominator.
法赞(MS Farzan)博士 他曾为知名的视频游戏公司和编辑网站(例如,Electronic Arts,Perfect World Entertainment,Modus Games和MMORPG.com)撰写和工作,并曾担任《龙与地下城:龙骨无双》和《 质量效应:仙女座》等游戏的社区经理。 。 他是《 Entronancy:Cyberpunk Fantasy RPG》的创意总监和首席游戏设计师,并且是《 The Nightpath Trilogy》的作者。 在Twitter @sominator上找到MS Farzan 。
翻译自: https://www.freecodecamp.org/news/how-to-build-a-multiplayer-tabletop-game-simulator/