基于Django + Vue 的术士之战游戏

 

支持在线联机对战的简易版吃鸡游戏,前后端分离,前端支持小程序端和Web端。技术栈:Django,WebSocket,微服务,Vue,Redis,nginx。代码编写在云服务终端完成

功能包括:

  • 游戏菜单、游戏设置:熟悉http协议
  • 存储对局信息、账户信息:熟悉数据库操作
  • 在Redis中存储对局状态:熟悉内存数据库操作
  • 在线聊天室、实时移动、实时放技能:熟悉websocket协议
  • 在线匹配系统:熟悉thrift和微服务
  • 配置nginx:熟悉nginx部署云服务

项目逻辑:通过acapp/acapp/urls.pypath('', include('game.urls.index')),找到acapp/game/urls/index.py,通过该文件中的 from game.views.index import indexpath("", index, name="index")找到acapp/game/views/index.pyindex函数:

def index(request):                                                                              
    return render(request, "multiends/web.html") 

请求web.html结合,生成完整的 HTML 页面并返回给浏览器:

找到acapp/game/templates/multiends/web.html,该文件引入了acapp/game/static/js/dist/game.js(/acapp/scripts/compress_game_js.sh脚本将js/src/下的js文件打包得到一个js/dist/game.js文件,减少http请求js文件的数量)

通过game.js渲染web.html,看到前端页面:

game.js的主类先实例化了settings对象:构造器里执行start()函数,调用getinfo()函数,通过ajax向后端发送请求,通过url找到对应函数并执行,并向前端返回Json字典resp

Docker环境配置

购买云服务后,新建用户wky,并配置别名wkyserver和免密登录,将django_lesson_1_0.tar传到云服务器(具体见租云服务器及配置Docker环境)

docker load -i django_lesson_1_0.tar
docker run -p 20000:22 -p 8000:8000 --name django_server -itd django_lesson:1.0 # 记得开放这两个端口,20000端口用于ssh登录,8000端口用于调试
docker attach django_server
adduser wky
usermod -aG sudo wky

然后ctrl+p, ctrl+q挂起容器,回到云服务器层

ssh wky@localhost -p 20000  # 在云服务器层ssh连接到docker层的wky,测试20000端口是否开放

然后logout(ctrl+d)回到云服务器层,再logout回到本地终端

配置别名djangoserver:

cd .ssh
vim config
Host djangoserver
        HostName 39.99.43.230
        User wky
        port 20000

配置免密登录

ssh-copy-id djangoserver

回到本地终端:将配置文件.bashrc .vimrc .tmux.config传到djangoserver

git并创建项目

生成ssh-key,将秘钥传到github

cd
ssh-keygen
cd .ssh
cat id_rsa.pub

创建django项目:

django-admin startproject acapp
cd acapp
git init
git config --global user.name "kaiyanWang"
git config --global user.email "321202864@qq.com"
git remote add origin git@github.com:kaiyanWang/acapp.git

运行

python3 manage.py runserver 0.0.0.0:8000

如果发现报错,将云服务器ip写到allowed_hosts里(字符串形式写):

vim acpp/setting.py

浏览器打开云服务器ip:8000,出现django页面,表示运行成功。

创建game应用

python3 manage.py startapp game

项目系统设计(game目录下)

  • menu:菜单页面
  • playground:游戏界面
  • settings:设置界面

项目文件结构(game目录下,将views.py,urls.py,models.py删除,新建目录)

  • urls 目录:管理路由,即链接与函数的对应关系
  • views 目录:管理 http 函数
  • models 目录:管理数据库数据(urls,views,models目录下都是python文件,因此要加__init__.py)
  • templates 目录:管理html文件
  • static 目录:管理静态文件,比如:
    • css:对象的格式,比如位置、长宽、颜色、背景、字体大小等
    • js:对象的逻辑,比如对象的创建与销毁、事件函数、移动、变色等
    • image:图片
    • audio:声音
    • ……
  • consumers 目录:管理 websocket 函数

更改时区:在acapp/acapp/settings.py中,将TIME_ZONE改为’Asia/Shanghai’

让Django 框架识别并启用 game 应用:将acapp/game/apps.py中的GameConfig加到acapp/acapp/settings.py中的INSTALLED_APPS:’game.apps.GameConfig’

设置静态文件地址(static里存开发者文件,media存用户文件):在acapp/acapp/settings.py添加 import os STATIC_ROOT = os.path.join(BASE_DIR, ‘static’)

MEDIA_ROOT = os.path.join(BASE_DIR, ‘media’) MEDIA_URL = ‘/media/’

图片下载方式:wget --output-document=自定义图片名称 图片地址

  • jquery 库:

nginx

配置nginx之前:通过8000端口访问Django项目

配置nginx之后,通过443或80端口访问nginx,再通过uwsgi访问Django项目

数据库表注册

在项目的根目录下使用以下命令可以用tab查看补全

python3 manage.py shell

每张数据库表都对应Django里的一个class acapp/game/admin.py可以将自定义的数据表注册到管理员页面,更新数据库:

python3 manage.py makemigrations
python3 manage.py migrate

写函数(每个函数对应一个url)

每次写函数都要修改3个文件:views,urls,js

第三方授权登录

OAuth 2.0 —— 它是目前最主流的「第三方授权登录 / API 授权」协议,核心作用是让用户无需暴露账号密码,就能授权第三方应用访问其在另一平台的资源。

OAuth 2.0 核心流程

  • 点击 “第三方平台一键登录”,client向server发送请求,server端给用户返回apply_code_url(https://github.com/login/oauth/authorize/?client_id=Ov23li2n3MttGt46yXlq&redirect_uri=http%3A//39.99.43.230%3A8000/settings/github/receive_code/)跳转到第三方授权页面;
  • 用户点击第三方平台授权按钮,执行redirect_uri对应的函数:
    • 第三方平台给server返回一个「授权码(code)」;
    • server通过第三方平台的access_token_url(参数需要code,clent_id,client_secret)获取access_token,再通过userinfo_url(需要access_token)获取用户信息
    • 注册用户并直接登录

在安装 clash 的本地电脑上运行: ssh -NfR 7890:localhost:7890 -p 57457 root艾特服务器地址

在服务器上配置代理: export https_proxy=http://127.0.0.1:7890 export http_proxy=http://127.0.0.1:7890

redis

sql数据库:关系型数据库,基于磁盘存储,存储的是数据库表

redis:内存型键值数据库,数据主要存储在内存中,存储的是key, value对

redis:存储的是key, value对;单线程,不会出现读写冲突;

在Django中集成Redis

step 1. 安装 django_redis

pip install django_redis

step 2. 配置 settings.py

CACHES = {
    'default': {
        'BACKEND': 'django_redis.cache.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        },
    },
}
USER_AGENTS_CACHE = 'default'

step 3. 启动 redis-server

sudo redis-server /etc/redis/redis.conf

可以使用:python3 manage.py shell操作redis:

from django.core.cache import cache

cache.keys('*')  # 找所有的key
cache.set('wky', 1, 5)  # 设置关键字,5秒之后过期
cache.has_key('wky')  # 查看key为wky是否存在
cache.get('wky')  # 查看wky的值
cache.delete('wky')  # 删除key为wky的键值对

联机对战

1、统一长度单位 2、配置django_channels(实现websockets,websockets是websocket的安全协议)

step1. 安装 channels_redis:

pip install channels_redis

step2. 配置 acapp/asgi.py 内容如下:

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from game.routing import websocket_urlpatterns

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'acapp.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket": AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
})

step3. 配置 acapp/settings.pyINSTALLED_APPS 中添加 channels,添加后如下所示:

INSTALLED_APPS = [
    'channels',
    'game.apps.GameConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

在文件末尾添加的内容如下:

ASGI_APPLICATION = "acapp.asgi.application"
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

step4. 配置 game/routing.py

这一部分的作用相当于 http 的 urls。

from django.urls import path

websocket_urlpatterns = [
]

step5. 编写 game/consumers

这一部分的作用相当于 http 的 views。

from channels.generic.websocket import AsyncWebsocketConsumer
import json

class MultiPlayer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.accept()
        print('accept')

        self.room_name = "room"
        await self.channel_layer.group_add(self.room_name, self.channel_name)

    async def disconnect(self, close_code):
        print('disconnect')
        await self.channel_layer.group_discard(self.room_name, self.channel_name);

    async def receive(self, text_data):
        data = json.loads(text_data)
        print(data)

step6. 启动 django_channels 在 ~/acapp 目录下执行:

daphne -b 0.0.0.0 -p 5015 acapp.asgi:application

3、编写同步函数 create_player、move_to、shoot_fineball、attack、blink

点击“多人模式”,相当于在地址栏输入url:ws://39.99.43.230:5015/ws/multiplayer/,对应后端函数MultiPlayer.as_asgi(),建立websocket连接,双向传送数据。

同步逻辑:点击后多人模式后,前端执行create_player()创建自己之后,还会send_create_player(),发送数据给后端,后端通过receive(self, text_data)函数接受前端发来的消息,并判断类型,向前端每个窗口广播await self.create_player(data),每个窗口接收消息后(前端js的start()函数执行了receive()函数,receive()函数中的WebSocket.onmessage 绑定回调函数(监听服务器推送的消息),这个绑定操作只需要执行 1 次即可生效,接收服务器的请求),判断是否是自己,如果不是就在本窗口执行create_player()函数创建用户。

为了防止状态不同步,攻击只能由一个窗口判断,然后广播给其他窗口,并且被击中的球的位置由攻击者决定(被击中会有击退效果)

例:同步move_to函数 前端js

    send_move_to (tx, ty) {                                                               
        let outer = this;
        this.ws.send(JSON.stringify({
            'event': "move_to",
            'uuid': outer.uuid,
            'tx': tx, 
            'ty': ty
        }));
    }   

    receive_move_to (uuid, tx, ty) {
        let player = this.get_player(uuid);

        if (player) {
            player.move_to(tx, ty);
        }
    }   

在原来的监听键盘q+鼠标左键后调用发送函数,传给服务器。

后端接受前端发送的消息,判断类型

    async def receive(self, text_data):
        data = json.loads(text_data)
        event = data['event']
        if event == "create_player":                                                                                                                                                                             
            await self.create_player(data)
        elif event == "move_to":
            await self.move_to(data)

前端移动后,发送自己的移动信息给后端

if (outer.playground.mode === "multi mode") {
    outer.playground.mps.send_move_to(tx, ty);  // 广播:移动
}  

启动Django后端:python3 manage.py runserver 0.0.0.0:8000 启动联机服务:daphne -b 0.0.0.0 -p 5015 acapp.asgi:application 将所有的js文件打包:./scripts/compress_game_js.sh