Django框架下的游戏项目
Django
项目的介绍
项目模式
MVC
模式:
views处理视图一切逻辑,model处理一切数据逻辑(存,取,算),controller最简单,仅仅作为v和m的胶水其他任何事情都不做。
假设这里有一个View会提交数据给Model进行处理以实现具体的行为,View会先把数据提交给Controller,然后Controller再将数据转发给Model。假如此时程序业务逻辑的处理方式有变化,那么只需要在Controller中将原来的Model换成新实现的Model就可以了,控制器的作用就是这么简单, 用来将不同的View和不同的Model组织在一起,顺便替双方传递消息,仅此而已。
Django
的 MTV
模式本质上和 MVC
是一样的,也是为了各组件间保持松耦合关系,只是定义上有些许不同,Django
的 MTV
分别是指:
- M 表示模型(
Model
):编写程序应有的功能,负责业务对象与数据库的映射(ORM
)。 - T 表示模板 (
Template
):负责如何把页面(html
)展示给用户。 - V 表示视图(
View
):负责业务逻辑,并在适当时候调用Model
和Template
。 - 如果views函数中不存在数据调用,那么views视图会直接返回一个模板也就是html文件给前端网页。
- 如果存在数据调用,那么views会调用model去数据库中查找数据。
访问页面路径
Django
中有层级路由的概念,也就是我们可以通过在项目根目录下的url
文件中将下层目录的url
路径include
进来,避免了我们必须将所有url
路径都保存在根目录下的url
文件内。
在urls
文件夹下的根目录下的index.py
文件内,Django
寻找名为urlpatterns
的变量并且按序匹配正则表达式。在找到匹配项 ,如'polls/'
,他就会将url
中对应部分切掉,并进入到下一层url
文件中继续匹配,使得Django
最终可以匹配到我们在views
内保存的函数。
前端页面页面
由于我的游戏项目是前后端分离的,所有的HTML
渲染工作都是在前端页面完成的。因此前端的开发流程就是,先在 HTML
里创建好一个有 id
的 div
,然后在执行js文件时捕获div
标签,并进行渲染。
前后端交互
前端实现动画效果
这里借用了unity
中MonoBehaviour
类的概念,即:将所有需要在前端实现移动的对象全部存到js
中object
类里面,并且每秒调用六十次requestAnimationFrame
函数。接下来所有的一切游戏功能,都是基于这个引擎的基类完成的,当我们要把一个js
类对象实现动画化,可以让其继承自MonoBehaviour
类,并借鉴unity
中update()
和start()
的概念,调用基类的构造函数,将自己加到object类中的数组中,实现动画化。
-
window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画。方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用
window.requestAnimationFrame()
。
创建用户系统
一个数据库驱动的 Web 应用的第一步就是定义模型。在我的游戏应用中,需要给玩家提供登录、注册等功能,要想实现这些功能,就需要给每名玩家在后台存储他的用户名,密码,头像等信息。
其中django
已经为我们提供了一个User
类。其中包含了username
、password
和name
等属性,能够满足我们创建用户()和验证用户的需求。我们仅仅需要再添加一个类来储存用户的头像和一键登录授权信息即可
#创建用户: 使用 create_user() 函数
>>> from django.contrib.auth.models import User
>>> user = User.objects.create_user('john', 'lennon@thebeatles.com', 'johnpassword')
# At this point, user is a User object that has already been saved
# to the database. You can continue to change its attributes, if you want to change other fields.
>>> user.last_name = 'Lennon'
>>> user.save()
#验证用户: 使用 authenticate() 来验证用户。它使用 username 和 password 作为参数来验证
from django.contrib.auth import authenticate
user = authenticate(username='john', password='secret')
if user is not None:
# 数据库中已经储存了用户信息
else:
# 数据库中没有该玩家信息
1.static
我们采用的 前后端分离式 开发,所有的 html
渲染都要求在前端完成。
开发流程就是,现在 html
里创建好一个有 id
的 div
,然后利用 js
文件,捕获到该 div
,并进行渲染。
static/
文件夹下有css, js, image, audio
四个子文件夹。
直接向用户浏览器窗口发送js
文件,文件内容直接是网页浏览到的内容(需要先将URL/static
与本地/static
路径join在一起)
-
css
文件管理:一般一个工程只有一个css
文件夹就足够了。 -
js
文件管理:一个工程会有多个.js
文件,为了避免每次写新的.js
文件时import
每个文件,考虑将所有.js
文件存放在src
文件夹下,并将其所有.js
源文件通过创建一个脚本整合到dist
文件夹中。 -
html
文件管理
在templates
文件夹下创建的multiends
文件夹,用来存放不同终端下的html
文件,并返回给view
2.views
一类具有相同功能和模板的网页的集合,每个视图必须要做的只有两件事:返回一个包含被请求页面内容的 HttpResponse
对象,或者抛出一个异常,比如 Http404
。
在Django
中,网页和其他内容都是从视图派生而来。Django
将会根据用户请求的 URL
来选择使用哪个视图(更准确的说,是根据URL
中域名之后的部分)
- 存放各种函数,等待用户访问。
- 在
views
文件夹下创建了三个模块的文件夹:menu, playground, settings
__init__.py
:
__init__.py
文件的作用是将文件夹变为一个Python模块, Python 中的每个模块的包中,都有__init__.py
文件- 我们在导入一个包时,实际上是导入了它的
__init__.py
文件。这样我们可以在__init__.py
文件中批量导入我们所需要的模块,而不再需要一个一个的导入。
index.py
:
为了在返回html
文件到用户浏览器,我们需要创建一个index.py
文件,返回templates
中multiends
文件夹下的html
文件。
3.urls
服务器端跟据url
格式,来选择调用什么函数。
- 路由规则
/-- "" -- index
/ -- "menu/" -- menu.index
/ "" --> "game.urls" -->
/ \ -- "playground/" -- playround.index
id:scoket -> \-- "settings/" -- settings.index
\
\ "/admin" -- 到达管理员页面
- 修改路由起点文件:
~/ACAPP/app/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns =[
path('', include('game.urls.index')),
path('admin/', admin.site.urls),
]
- 再找到我们的第二分支
~/ACAPP/game/urls/index.py
from django.urls import path, include
from game.views.index import index
urlpatterns = [
path("", index, name = "index"),
path("menu/", include("game.urls.menu.index")),
path("playground/", include("game.urls.playground.index")),
path("settings/", include("game.urls.settings.index")),
]
至此,我们修改完了我们期望的路由规则
4.models
存放项目数据结构,如数据库里的table
,可以理解为各种class
每个模型被表示为 django.db.models.Model
类的子类。每个模型有许多类变量,它们都表示模型里的一个数据库字段。我们将会在 Python
代码里使用它们,而数据库同时会将它们作为列名。
5. templates
项目的 TEMPLATES
配置项描述了 Django
如何载入和渲染模板。
-
调用
templates
内的HTML
文件:在
views
函数内载入templates/index.html
模板文件,并且向它传递一个上下文(context
)。这个上下文是一个字典,它将模板内的变量映射为Python
对象。 -
「载入模板,填充上下文,再返回由它生成的
HttpResponse
对象」是一个非常常用的操作流程。
请求与响应
Django
使用请求和响应对象在系统中传递状态。
- 当一个页面被请求时,
Django
会创建一个HttpRequest
对象,这个对象包含了请求的元数据。然后,Django
加载相应的视图,将HttpRequest
作为视图函数的第一个参数。每个视图负责返回一个HttpResponse
对象。
HTTP
定义了与服务器进行交互的不同方法,常见的有四种:GET
、POST
、PUT
、DELETE
。其中,GET
和POST
最常用。
-
GET
用来获取资源,它只是获取、查询数据,不会修改服务器的数据,从这点来讲,它是安全的。由于它是读取的,因此可以对GET
请求的数据进行缓存。 -
POST
则是可以向服务器发送修改请求,进行数据的修改的。举个例子:比如说我们要在知乎、或者论坛下面评论,这个时候就需要用到POST
请求。但是它不能缓存,为什么呢?设想如果我们将“评论成功”的页面缓存在本地,那么当我发送一个请求的时候,直接返回本地的“评论成功”页面,而服务器端则什么也没有做,根本没有进行评论的更新,岂不是难以想象。
区别
运行服务器相关
-
uwsgi
前:django3 manage.py runserver 0.0.0.0:8000
-
运行
django
服务器的时候,突然远程连接断掉了,django
服务器关闭不了,下面将展示如何关闭
- 重新启动服务器显示端口已被占用
python3 manage.py runserver 0.0.0.0:8000
- 现在我们要查找是哪个进程占用了8000端口,然后
kill
掉
netstat -atunp//查看进程端口号及运行的程序
- 根据
pid
杀死指定进程:
kill 123456//pid号
将阿里云服务器重启之后发生了什么:
出现的问题:
-
docker镜像进入
exit
态,即dicker内所有进程全部关闭,包括但不限于uwsgi
,nginx
和redis
。 -
重启docker后,重新运行
uwsgi
,nginx
,redis
项目网页仍然无法正常打开。 -
将之前的项目文件删除,并重新从
GitHub
上拉取之前的备份。 -
启动
uwsgi
服务器,项目网页有部分模块失效。(原先支持登录,登出,注册和第三方一键登录,现在只能登录登出)- 原因是之前没有启动
redis
服务器,很明显的道理,网页只有一部分功能失效,说明传给浏览器的.js
文件肯定是没有出错的,已经上传的js
代码没有问题,最坏的情况也就是缺少一部分代码。 - 第一次没想到重启
redis
服务,一键登录系统一直出问题,直到我从头开始复盘整个一键登录模块实现流程,才发现可能是没有启动redis
服务,我打开python3 manage.py shell
尝试redis
命令,果然报错,启动redis
服务后一键登录模块重新上线!
- 原因是之前没有启动
-
根据Chrome网页的源代码报错和
js
代码debug,我发现发现从GitHub
拉取的备份.js
代码真的少了一部分,可能是之前上传的时候出现了问题。将缺少的js
文件和url.py
文件补充好后,项目重新上线并正常运行。
我的收获:
- 善于从网页报错和项目层级结构来寻找问题。比如网页提示
.js
文件出现问题,那么我会从构造函数的变量定义开始,再到start()
函数内实现的监听函数中分别查看各个子模块函数的定义是否有问题。 - 从顶向下查看
url
文件。根据Django
的分层路由特性,我们可以从settings
根目录下的index.py
文件开始,向下一层一层的查找相应子目录的index.py
文件。 - 每次
Git
上传当天工作,我们一定要善于在GitHub
上浏览我们的代码,速度会比在本地vim
上快很多。(可能跟电脑也有关系xD)
项目环境初始化
文件结构
$ python3 manage.py startapp game
$ tree .
> .
> |-- acapp #Django-admin 新建一个 Django 项目
> | |-- __init__.py
> | |-- __pycache__
> | | |-- __init__.cpython-38.pyc
> | | |-- settings.cpython-38.pyc
> | | |-- urls.cpython-38.pyc
> | | `-- wsgi.cpython-38.pyc
> | |-- asgi.py
> | |-- settings.py
> | |-- urls.py
> | `-- wsgi.py
> |-- db.sqlite3
> |-- game
> | |-- __init__.py
> | |-- admin.py # django网页后台管理员页面
> | |-- apps.py # 用的不多
> | |-- migrations
> | | `-- __init__.py
> | |-- models.py # 定义网站里的数据库表
> | |-- tests.py
> | `-- views.py # 视图,即函数
> `-- manage.py
配置nginx
和uwsgi
服务器
nginx
的作用
1. 静态HTTP服务器
- 首先,
Nginx
是一个HTTP服务器,可以将服务器上的静态文件(如HTML、图片)通过HTTP协议展现给客户端。
2. 反向代理服务器
- 客户端本来可以直接通过HTTP协议访问某网站应用服务器,网站管理员可以在中间加上一个
Nginx
,客户端请求Nginx
,Nginx
请求应用服务器,然后将结果返回给客户端,此时Nginx
就是反向代理服务器。
那么我们为什么要多此一举呢?下面列举一下Nginx
的作用:
3. 负载均衡
- 当网站访问量非常大,网站会越来越慢,一台服务器已经不够用了。于是将同一个应用部署在多台服务器上,将大量用户的请求分配给多台机器处理。
- 带来的好处是,其中一台服务器万一挂了,只要还有其他服务器正常运行,就不会影响用户使用。
Nginx
可以通过反向代理来实现负载均衡。
4. 虚拟主机
-
对于访问流量巨大的网站来说,他们需要负载均衡。但对于一些小网站来说,由于访问量较小,需要节约成本,将多个网站部署在一台服务器上。
-
例如将
http://www.aaa.com
和http://www.bbb.com
两个网站部署在同一台服务器上,两个域名解析到同一个IP
地址,但是用户通过两个域名却可以打开两个完全不同的网站,互相不影响,就像访问两个服务器一样,所以叫两个虚拟主机。 -
例如,我们在服务器端口8080和8081分别开了一个应用,客户端通过不同的域名访问,根据
server_name
可以反向代理到对应的应用服务器。虚拟主机的原理是通过HTTP请求头中的
Host
是否匹配server_name
来实现的。uWSGI
的作用WSGI
WSGI
的全称是Web Server Gateway Interface
(Web服务器网关接口),它不是服务器、python
模块、框架、API
或者任何软件,只是一种描述web
服务器(如nginx
,uWSGI
等服务器)如何与web
应用程序(如用Django、Flask
框架写的程序)通信的规范、协议。
当前运行在WSGI
协议之上的web框架有,Flask, Django
。
uwsgi
与
WSGI
一样,是uWSGI
服务器的独占通信协议,用于定义传输信息的类型。每一个uwsgi packet
前4byte
为传输信息类型的描述,与WSGI
协议是两种东西,据说该协议是fcgi
协议的10倍快。uWSGI
uWSGI
是一个全功能的HTTP
服务器,实现了WSGI
协议、uwsgi
协议、http
协议等。它要做的就是把HTTP
协议转化成语言支持的网络协议。比如把HTTP
协议转化成WSGI
协议,让Python
可以直接使用。
- 中间的反向代理服务器就是
Nginx
服务器,右边三台web服务器是uwsgi
服务器。
通信协议--WEB-Scoket
-
我们已经有了 HTTP 协议,为什么还需要另一个协议?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。而
WebSocket
能够实现全双工通信,可以由服务端主动发起消息,对于浏览器需要及时接收数据变化的场景非常适合-
例如在
Django
中遇到一些耗时较长的任务我们通常会使用async
来异步执行,那么浏览器如果想要获取这个任务的执行状态,在HTTP协议中只能通过轮训的方式由浏览器不断的发送请求给服务器来获取最新状态,这样发送很多无用的请求不仅浪费资源,还不够优雅,如果使用WebSokcet
来实现就很完美了 -
WebSocket
的另外一个应用场景就是我们实现的另一个模块--聊天室。一个用户(浏览器)发送的消息需要实时的让其他用户(浏览器)接收,这在
HTTP
协议下是很难实现的,但WebSocket
基于长连接加上可以主动给浏览器发消息的特性处理起来就游刃有余了
-
-
Django
如何实现web_socket
?Django
本身不支持WebSocket
,但可以通过集成Channels
框架来实现WebSocket
。
创建菜单menu
界面
0.实现游戏引擎
- 游戏中,物体在移动,其实现原理是:每一个动作都会渲染多张图片出来,然后图片快速的切换,从而实现移动的过程。 因此,需要先实现一个游戏引擎的基类
AcGameObject
,使得每帧能渲染一张图片出来.
- 在
~/ACAPP/game/templates/multiends/web.html
里创建一个带有id的div。给名字(id)的目的是我们以后可用js
来控制它, 比如说移动它或改变它的一些性质等等。
Django
路由
- 我们在前端访问网页的路径是由
/urls
提供的,我们要知道,一个文件有许多不同的模块,比如settings
,menu
,playground
等。 - 因此
django
提供了层级路由,上层文件下的index.py
可以调用下层目录的index.py
中的path
和include
。十分方便
Django
后端提供的服务
联机模式下的玩家信息同步
玩家进入房间
- 双向连接:
http
是单向连接,这里我们需要双向连接协议websocket
https:
加密协议 wss
加密
django
自带数据库splite3
,面向对象的方式更改数据库里的表,存储在.py文件中
websocket
,即ws
协议连接server
和client
:用来联机对战和聊天
1.文件结构
-
game/rooting.py
文件夹功能等于urls
路由函数,我们在前端网页通过url
访问。 -
game/consumers
文件夹的功能等于views
函数,存放后端服务器的所有异步函数。 -
前端向后端发送消息:调用函数
ws.send()
,后端通过index.py
中的reveive()
函数接收。
2.标识元素
-
一场游戏里,所有的元素(玩家,火球等)都需要唯一的标识
uuid
,来方便同步。为此,我们可以直接修改一下游戏引擎ac_game_object.js
,对于每个元素都创建我们需要的唯一uuid
。 -
通过广播实现玩家之间的信息同步,在哪个前端创建的对象,就以谁为准。使得每个窗口内,逻辑上相同的元素,其
uid
也相同即可:前端->
服务器->
群发消息。 -
正常的游戏引擎是同步每一个玩家窗口的所有信息,通过传递所有物体的坐标来实现。我的游戏引擎是通过同步玩家的操作,效率更高,服务器压力更小。
-
通过
redis
存每局信息,实现玩家同步。
3.django
异步函数编程
- 同步是指完成事务的逻辑,先执行第一个事务,如果阻塞了,会一直等待,直到这个事务完成,再执行第二个事务,顺序执行。
- 异步是和同步相对的,异步是指在处理调用这个事务之后,不会等待这个事务的处理结果,直接处理第二个事务去了,通过状态、通知、回调来通知调用者处理结果。
async def
用来定义异步函数,await (x)
表示当前协程任务等待睡眠时间x
,此时会将该线程阻塞,允许其他任务运行。并获得一个事件循环,主线程调用asyncio.get_event_loop()
时会创建事件循环,你需要把异步的任务丢给这个循环的run_until_complete()
方法,事件循环会安排协同程序的执行。
玩家移动(通过ws
协议调用后端函数实现流程)
-
js
中实现send
和receive
函数,在前端实现发送信息和接收信息。 -
为了让游戏界面中对于要移动的元素做出移动动作,需要对
move_to
函数做出一些修改。首先要标识出当前为多人模式,然后在用户窗口检测到鼠标点击事件,并且模式为多人模式时,此时每次移动都会向
uwsgi
协议下的js
函数触发一次通信。 -
以实现
move_to
函数为例,- 当
js
检测到窗口的点击鼠标事件,会调用js
文件内的玩家移动函数,实现在本窗口内的玩家移动。 - 当
js
判断当前游戏模式是多人模式时,会路由到websocket
协议下的send_move_to(tx, ty).js
函数,向服务器发送一个JSON
事件。 - 在服务器后端
python async
异步函数接收到websocket
发送的消息,根据JSON
字典中的值,路由到不同的函数,如move_to(uuid, tx, ty).python
函数,并通过实现向所有玩家layer
群发消息。 - 在前端的
receive_move_to().js
函数收到消息,通过唯一标识符uuid
来判断是哪个玩家发生移动,从而实现这名玩家在所有layer
中移动。
- 当
玩家发射火球
前端监听window
对象,也就是整个playground
界面。
为了只让一个客户端进行攻击命中的判断,因此只有发出方的火球才做碰撞检测。
其他客户端对于该火球只有动画效果
又由于碰撞检测是在一台客户端上进行的,因此多端之间可能会存在同步上的延迟
为此的解决方法是:碰撞检测成功时,强制把被击中玩家移动到发起方客户端中的位置,以避免击中延迟上发生的事情
判断是否被火球击中
状态机:只有在fighting状态下才允许监听函数运行
通知牌:
父类:
渲染:
聊天系统
前端部分:
window和canvas的监听事件不同:
在canvas中加入聚焦属性。
聚焦:
打开、关闭聊天框
历史消息记录
后端部分:
thrift系统
一切不能阻塞,或需要第三方服务的功能都需要thrift
服务。