【Unity学习笔记】多人生存社交轻网游《夕立求生》


游戏介绍


《夕立求生》是一款多人生存AVG社交轻网游,同时支持多人混战,并可以随时随地与朋友来上一局的聚会型对战游戏

游戏截图


 

更新记录


0.37 Unity版首发

0.38 修复一些Bug

0.40 全面更新反馈方式,采取自动查询,敌人攻击反馈

0.41 更新图片反馈,增加击中抖动,被击中文字提示等,新版UI上线

游戏下载


PC版、手机版下载

QQ机器人版专用群


技术栈

  • 客户端:unity
  • 服务器端:python
  • 数据库:Mysql

代码分析


Unity:

客户端主程序:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Text;
using UnityEngine.UI;
using System.Text.RegularExpressions;

public class clientuse : MonoBehaviour {
  Socket clientSocket;
  byte[] sdata = new byte[1024];
  byte[] data =new byte[1024];
  Thread t;
  int hp;
  public int timenumber;
  public AudioSource[] se;
  public IPEndPoint ipep;
  bool connect=false;
  public Text text;
  public Text namebox;
  public string str;
  string playername;
  public GameObject startview;
  public GameObject returnview;
  public GameObject exportview;
  public Text xsview;
  string outword;
  public int accept;
  public void sendcx(){
    se[0].Play();
    if (connect == true) {
      data = Encoding.UTF8.GetBytes ("createnewone");
      clientSocket.Send (data, data.Length, SocketFlags.None);
    }
  }
  public void sendcxtime(){
    //Debug.Log("自动查询");
    if (connect == true) {
      data = Encoding.UTF8.GetBytes ("cx");
      clientSocket.Send (data, data.Length, SocketFlags.None);
    }
  }
  public void sendts(){
    if (connect == true && accept==0) {
      se[1].Play();
      accept = 40;
      data = Encoding.UTF8.GetBytes ("getexp");
      clientSocket.Send (data, data.Length, SocketFlags.None);
    }
  }
  public void sendzk(){
    se[0].Play();
    if (connect == true) {
      data = Encoding.UTF8.GetBytes ("lookupmember");
      clientSocket.Send (data, data.Length, SocketFlags.None);
    }
  }
  public void sendname(){
    data = Encoding.UTF8.GetBytes (playername);
    clientSocket.Send (data, data.Length, SocketFlags.None);
  }

  public void Connect(){
    se[0].Play();
    playername = namebox.text;
    ipep = new IPEndPoint (IPAddress.Parse ("116.196.107.238"), 9090);
    clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    clientSocket.Connect(ipep);
    connect = true;
    Destroy (startview);
    returnview.active= true;
    exportview.active= true;
  }

  string translateml(string inputword){
    //Debug.Log (hp);
    string outputword;
    string[] inputdata;
    outputword="";
    if (inputword.Contains ("lookup#")) {
      inputdata = inputword.Split(new char[1] {'#'});
      outputword = "ID:" + inputdata [2] + " 经验:" + inputdata [4] + " 生命值:" + inputdata [6] + " 攻击力" + inputdata [8];
      if (hp > int.Parse (inputdata [6])) {
        se [2].Play ();
        this.gameObject.SendMessage ("fuji");
        this.gameObject.SendMessage ("Sock");
      }
      hp=int.Parse(inputdata[6]);
    }
    if (inputword.Contains ("noname#")) {
      if (hp > 0) {
        outputword = "ID:被别的玩家行动击杀!";
        this.gameObject.SendMessage ("fukl");
        this.gameObject.SendMessage ("Sock");
        se [2].Play ();
        hp = 0;
      } else {
        outputword = "ID:未创建角色或角色已死亡";
      }
    }
    if(inputword.Contains("create#")){
      inputdata = inputword.Split (new char[1] {'#'});	
      outputword = "已新建角色" + inputdata [1];
      hp = 100;
    }
    if (inputword.Contains ("onlyone#")) {
      outputword = "当前游戏中只有一名玩家,不能进行探索\n您可以尝试使用【加入】创建角色,\n或是邀请朋友一起进行游戏";
    }
    if (inputword.Contains ("get1#")) {
      inputdata = inputword.Split (new char[1] {'#'});
      this.gameObject.SendMessage ("showpic", "sd", SendMessageOptions.DontRequireReceiver);
      outputword = "爽到,获得" + inputdata [1]+"经验";			
    }
    if (inputword.Contains ("lost1#")) {
      inputdata = inputword.Split (new char[1] {'#'});
      this.gameObject.SendMessage ("showpic", "bkcr", SendMessageOptions.DontRequireReceiver);
      outputword = "追尾了黑色高级轿车,损失" + inputdata [1]+"经验";			
    }
    if (inputword.Contains ("get2#")) {
      inputdata = inputword.Split (new char[1] {'#'});	
      this.gameObject.SendMessage ("showpic", "csg", SendMessageOptions.DontRequireReceiver);
      outputword = "击到了触手怪,获得" + inputdata [1]+"经验";			
    }
    if (inputword.Contains ("get3#")) {
      this.gameObject.SendMessage ("showpic", "none", SendMessageOptions.DontRequireReceiver);
      outputword = "什么也没发现,向前走走吧";			
    }
    if (inputword.Contains ("lost2#")) {
      se [3].Play();
      //this.gameObject.SendMessage ("Sock");
      this.gameObject.SendMessage ("showpic", "dk", SendMessageOptions.DontRequireReceiver);
      inputdata = inputword.Split (new char[1] {'#'});	
      outputword = "被敌人偷袭,损失" + inputdata [1]+"血量";	
      hp -= int.Parse (inputdata [1]);
    }
    if (inputword.Contains ("get4#")) {
      inputdata = inputword.Split (new char[1] {'#'});	
      this.gameObject.SendMessage ("showpic", "mb", SendMessageOptions.DontRequireReceiver);
      outputword = "捡到了面包,增加" + inputdata [1]+"血量";
      hp += int.Parse (inputdata [1]);
    }
    if (inputword.Contains ("lost3#")) {
      this.gameObject.SendMessage ("showpic", "rua", SendMessageOptions.DontRequireReceiver);
      outputword = "踩到屎了,损失50血量,rua";
      hp -= 50;
    }
    if (inputword.Contains ("meet#")) {
      se [3].Play();
      this.gameObject.SendMessage ("showpic", "enemy", SendMessageOptions.DontRequireReceiver);
      inputdata = inputword.Split (new char[1] {'#'});	
      outputword = "遇到了"+inputdata [1]+"\n给对方造成"+inputdata[3]+"伤害";
      if (inputword.Contains ("kill#")) {
        outputword = outputword + "\n已经击杀对方,场上剩余人数" + inputdata [5];
      }
      if (inputword.Contains ("win#")) {
        outputword = outputword + "\n大吉大利,今晚吃鸡";
        hp = 100;
      }
    }
    if (inputword.Contains ("nopeople#")) {
      this.gameObject.SendMessage ("showpic", "none", SendMessageOptions.DontRequireReceiver);
      outputword = "远处一阵风吹草动,然而并没有看到人";			
    }
    if (inputword.Contains ("get5#")) {
      this.gameObject.SendMessage ("showpic", "wqsj", SendMessageOptions.DontRequireReceiver);
      outputword = "捡到了新的武器,攻击力+5!";			
    }
    if (inputword.Contains ("death#")) {
      string getlastpoint = Regex.Match (inputword, "(?<=death#).*?(?=#)").Value;
      outputword = outputword+"\n血量低于0,角色已经死亡,请重新创建角色\n本次得分是:"+getlastpoint;			
    }
    if (inputword.Contains ("lastone#")) {
      string getlastman = Regex.Match (inputword, "(?<=lastone#).*?(?=#)").Value;
      outputword = outputword+"\n游戏结束,场上仅剩余一人:"+getlastman;			
    }
    if (inputword.Contains ("nocha#")) {
      outputword ="角色不存在~\n可能是已被其他玩家击杀\n可尝试战况查看详细\n点击【加入】重新加入游戏";		
    }
    if (inputword.Contains ("number#")) {
      inputdata = inputword.Split (new char[1] { '#' });
      outputword ="剩余人数:"+inputdata[1]+"\n"+inputdata[3];		
    }
    return outputword;
  }
  // Use this for initialization
  void Start () {
    exportview.active= false;
    returnview.active= false;
    accept = 0;
    timenumber =0;
  }
  
  // Update is called once per frame
  void Update ()
  {
    if (accept>0){
      accept--;
    }
    if (connect == true) {
      int bufLen = 0;
      bufLen = clientSocket.Available;
      if (bufLen == 0) {

      } else {
        clientSocket.Receive (sdata, 0, bufLen, SocketFlags.None);
        string clientcommand = System.Text.Encoding.UTF8.GetString (sdata).Substring (0, bufLen);
        if (clientcommand == "please input your player name") {
          sendname ();
          outword = "已经与服务器建立连接,并登陆ID";
          text.text = "已经与服务器建立连接,并登陆ID";
        }else{
          //Debug.Log (clientcommand);
          outword = translateml (clientcommand);
          if (outword.Contains ("ID:")) {
            xsview.text = outword;
          }else{
            returnview.SendMessage ("getword", outword, SendMessageOptions.DontRequireReceiver);
          }
        }
      }      
    }
  }
}

这次第一次UnityTCP客户端上面增加了发送模块~但是Update还是只用于监听,采取了使用UGUI控制或是用其他的.cs文件控制激发发送数据实现通信。接收到的数据采用了translateml子函数来翻译成显示文字。虽然实际上QQ机器人版本的输出显示也能直接传递,但是英文命令可以增加方便分析的Key,使得接受的命令中的信息更容易分析出来~。因为有部分数据的位置不确定,有些数据不能和往常一样简单的用Split函数进行分割,这里简单的采用了正则的方式分析进行显示:

string getlastman = Regex.Match (inputword, "(?<=lastone#).*?(?=#)").Value;

通过这种方式,能够匹配到lastone#与下一个#之间的内容进行提取~从而显示一些数据。翻译命令的过程中,程序也针对一些命令的效果进行对应的游戏反馈,调用显示图片的函数来进行显示反馈等~

效果显示:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class sockcamera : MonoBehaviour {
  public GameObject showui;
  GameObject showobj;
  int socktime=0;
  public void Sock(){
    socktime=15;
  }
  public void fuji(){
    showobj = (GameObject)Resources.Load ("zyfj");
    GameObject outfuji;
    outfuji = Instantiate (showobj);
    outfuji.transform.parent = showui.transform;
  }
  public void fukl(){
    showobj = (GameObject)Resources.Load ("zykl");
    GameObject outfuji;
    outfuji = Instantiate (showobj);
    outfuji.transform.parent = showui.transform;

  }
  public void showpic(string picname){
    showobj = (GameObject)Resources.Load ("picture");
    GameObject outfuji;
    outfuji = Instantiate (showobj);
    outfuji.transform.parent = showui.transform;
    SpriteRenderer spr = outfuji.GetComponent<SpriteRenderer> ();
    Texture2D texture2d = (Texture2D)Resources.Load (picname);
    Sprite sp = Sprite.Create (texture2d, spr.sprite.textureRect, new Vector2 (0.5f, 0.5f));
    spr.sprite = sp;
  }

  // Use this for initialization
  void Start () {
    socktime=0;
  }
  
  // Update is called once per frame
  void Update () {
    if (socktime > 0) {
      this.gameObject.transform.eulerAngles=new Vector3(0,0,((socktime%3)-1)*2);
      socktime--;
    }
  }
}

这里包含了调用各种显示效果的函数。包括了屏幕抖动,根据客户端命令调用图片预设的内容~其中,

SpriteRenderer spr = outfuji.GetComponent<SpriteRenderer> ();
Texture2D texture2d = (Texture2D)Resources.Load (picname);
Sprite sp = Sprite.Create (texture2d, spr.sprite.textureRect, new Vector2 (0.5f, 0.5f));

采用了动态的方式改变显示的图片名称,通过这种方式指定多个图片名动态加载~感觉就和橙光的字符串加载图片是差不多的。

自动显示与自动发送:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class showtext : MonoBehaviour {
  public GameObject cliobject;
  public Text text;
  string returnword;
  int lon;
  public int timenumber;
  // Use this for initialization
  void Start () {
    timenumber = 0;
    lon = 0;
    returnword="已经与服务器建立连接,并登陆ID";
  }
  
  // Update is called once per frame
  void Update () {
    timenumber++;
    if (timenumber == 40) {
      timenumber = 0;
      cliobject.SendMessage ("sendcxtime");
    }
    
    if (returnword != "已经与服务器建立连接,并登陆ID") {
      int leng = returnword.Length;
      if (lon < leng) {
        lon++;
        text.text = returnword.Substring (0, lon);
      }
    } else {
      text.text = "已经与服务器建立连接,并登陆ID";
    }
  }
  void getword(string msg){
    returnword = msg;
    lon = 0;
  }
}

与QQ机器人版不同的地方是,在Unity版中,会定时自动同步服务器上发回来的人物资料,并且计算本地血量和服务器血量的差值~如果服务器返回的血量值要低于本地计算的血量值,那么就一定存在别的玩家对玩家的血量造成了影响,即发生了伏击事件。

这个程序就包含了之前在从Excel读取剧情对话的打字机显示部分,以及定时让客户端发送查询血量的指令部分。

图片移动的逻辑和橙光动态UI类似,这里不作累数

服务器端:

#!/usr/bin/python
# coding=utf-8

import socket
import threading
import time
import random
import urllib2
import re
import json
import MySQLdb
import sys

#初始化
reload(sys)
sys.setdefaultencoding('utf-8')
rebattle=""

#游戏本体
def onLinkgame(comment,player):
  global rebattle
  db=MySQLdb.connect(host="localhost",user="root",passwd="",db="qqplay",charset="utf8")
  cursor=db.cursor()
  na='"'+player+'"'
  ml="select*from player where name="+na
  cursor.execute(ml)
  data=list(cursor.fetchall())
  print data
  if comment="cx":
      try:
      out="角色名:"+na+"\n经验值:"+str(data[0][1])+"\n生命值:"+str(data[0][2])+"\n攻击力:"+str(data[0][3])
    except:
      out="noname#"
  if comment=="createnewone":
    try:
      out="角色名:"+na+"\n经验值:"+str(data[0][1])+"\n生命值:"+str(data[0][2])+"\n攻击力:"+str(data[0][3])
    except:
      
      cursor.execute("insert into player values("+na+",01,100,20)")
      out="已新建角色"+na
      db.commit()
  if comment=="getexp":
    cursor.execute("select count(*) from player")
    mandata=cursor.fetchall()
    man=int(mandata[0][0])
    if man==1:
      out="当前只有1名玩家加入了游戏,不能进行探索,尝试邀请更多玩家进行游戏吧!"
      return out
    try:
      getpoint=0
      gethp=0
      event=random.randint(1,35)
      pst=True
      hst=True
      if event<2:
        getpoint=random.randint(1,15)
        out=na+"爽到,捡到"+str(getpoint)+"经验"
      elif event<4:
        pst=False
        getpoint=random.randint(1,5)
        out=na+"追尾了黑色高级车,损失了"+str(getpoint)+"经验"
      elif event<=6:
        getpoint=random.randint(1,5)
        out=na+"击倒了触手怪,获得了"+str(getpoint)+"经验"
      elif event<=8:
        getpoint=0
        out=na+"什么也没发现,向前走走吧"
      elif event<=14:
        hst=False
        gethp=random.randint(1,20)
        out=na+"被敌人偷袭,损失了"+str(gethp)+"血量"
      elif event<=20:
        gethp=random.randint(1,40)
        out=na+"捡到了面包,增加了"+str(gethp)+"血量"
      elif event<=21:
        hst=False
        gethp=50
        out=na+"踩到了屎,降低了50血量,rua"
      elif event<=28:
        cursor.execute("select * from player order by rand() limit 1")
        amydata=cursor.fetchall()
        amyna='"'+str(amydata[0][0])+'"'
        if amyna != na:
          out=na+"遇到了"+amyna
          out=out+"\n对对方造成"+str(data[0][3])+"伤害"
          cursor.execute("update player set hp=hp-"+str(data[0][3])+" where name="+amyna)
          if int(amydata[0][2])<int(data[0][3]):
            rebattle=rebattle+"\n玩家"+na+"击杀了"+amyna+",死亡玩家的经验是:"+str(amydata[0][1])
            cursor.execute("delete from player where name="+amyna)
            cursor.execute("select count(*) from player")
            mandata=cursor.fetchall()
            man=int(mandata[0][0])
            out=out+"\n对方死亡~获得了经验值60,场上现在还剩下"+str(man)+"人"
            cursor.execute("update player set exp=exp+60 where name="+na)
            if man==1:
              out=out+"\n大吉大利,晚上吃鸡!"
              rebattle="上一轮游戏由"+na+"吃鸡"
              cursor.execute("update player set exp=0,hp=100,atk=20 where name="+na)
              #cursor.execute("delete from player")
        else:
          out="远处一阵风吹草动,然而并没有看到人"
      elif event <=35:
        cursor.execute("update player set atk=atk+5 where name="+na)
        out=na+"捡到了新的武器,攻击力+5!"

      if pst==True:
        cursor.execute("update player set exp=exp+"+str(getpoint)+" where name="+na)
      else:
        cursor.execute("update player set exp=exp-"+str(getpoint)+" where name="+na)
      
      if hst==True:
        cursor.execute("update player set hp=hp+"+str(gethp)+" where name="+na)
      else:
        cursor.execute("update player set hp=hp-"+str(gethp)+" where name="+na)

      ml="select*from player where name="+na
      cursor.execute(ml)
      data=list(cursor.fetchall())

  
      if int(data[0][2])<=0:
        rebattle=rebattle+"\n玩家"+na+"因意外事件被击杀,他的最终得分为:"+str(data[0][1])
        out=out+"\n血量低于0,角色已经死亡,请重新创建角色\n本次得分是:"+str(data[0][1])
        cursor.execute("delete from player where name="+na)
        cursor.execute("select count(*) from player")
        mandata=cursor.fetchall()
        man=int(mandata[0][0])
        if man==1:
          cursor.execute("select * from player")
          amydata=cursor.fetchall()
          amyna='"'+str(amydata[0][0])+'"'
          out=out+"\n本轮游戏只剩下一名玩家:"+amyna+"游戏结束!"
          rebattle="上一轮游戏由"+amyna+"吃鸡"
          cursor.execute("update player set exp=0,hp=100,atk=20 where name="+amyna)
      db.commit()
    except:
      out="角色不存在,可能是没有创建或已被击杀"
  if comment=="lookupmember":
    cursor.execute("select count(*) from player")
    mandata=cursor.fetchall()
    print mandata
    out="剩余人数:"+str(mandata[0][0])+"\n"+rebattle
  db.close()
  return out

#链接处理
def tcplink(sock,addr):
    print("Accept new connect")
    sock.send(b'please input your player name')
    playername=""
    data=sock.recv(1024)
    playername=data.decode('utf-8')
    sock.send(b'%s,welcome to the game'%playername)
    while True:
      data=sock.recv(1024)
      comment=data.decode('utf-8')
      if comment=="exit"
        sock.send(b'exit the game')
        sock.close()
        break
      reback=onLinkgame(comment,playername)
      sock.send(b'%s',reback)


if __name__ == '__main__':
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.bind(('本地IP',9090))
    s.listen(8)
    print("waiting for connection...")

    while True:
      sock,addr=s.accept()
      t=threading.Thread(target=tcplink,args=(sock,addr))
      t.start();

服务器端采用了Python编写,这一次采用了Mysqldb与服务器本地MySQL数据库进行了联动查询,从而使同时与多名玩家进行交互获得了可能。

主要采用了Mysql的几条语句,新建条目,查询,随机抽取,更新进行数据库的操作


想要成为自己的未来