2015-04-11 18:27:33 wlj613613 阅读数 1098

书籍比较杂,有些不一定写的很好,
全部是英文的(中文书一般是不会有清晰电子版,你肯定知道的),
所以大家酌情下载喜欢的那几本看完就好了。都是别人的劳动成果,
学习有收获就好,希望仅供学习交流之用。


Apress.Learn.Unity.4.for.iOS.Game.Development.Jun.2013

Unity3D 4.x Cookbook

Unity Shaders and Effects Cookbook

Unity 3D Game Development by Example Beginner

Unity 2D Game Development

Pro.Unity.Game.Development.with.CSharp-2014

C# Game Programming Cookbook for Unity 3D - CRC Press (2014)

Practical Game Design with Unity and Playmaker

NGUI for Unity

Learning C# by Developing Games with Unity 3D Beginner’s Guide

Unity.Multiplayer.Games

Unity4.x Game AI Programming
Unity.Android.Game.Development.by.Example.Beginners.Guide

Creating E-Learning Games with Unity

Apress.Learn.Unity.for.2D.Game.Development.Oct.2013

Getting.Started.with.Unity.Aug.2013

Unity AI Programming Essentials

Unity Game Development Scripting

Unity 3D UI Essentials (介绍unity 新GUI内容)

EBOOK ALL

VIDEO:

Creating Mobile Games with Unity

祝学习愉快!

2018-03-14 15:39:37 MadBam_boo 阅读数 1469

Unity3d实战之Unity3d网络游戏实战篇(1):坦克构建

  • 阅读文章前请细读以下几点:
  • 学习书籍《Unity3d网络游戏实战》 罗培羽著 机械工业出版。
  • 本文是作者在学习过程中遇到的认为值得记录的点,因此引用的代码等资源基本出资罗培羽老师的书籍 以下为个人的学习简记,与诸君共论。
  • 由于U3D的官方案例Tank Tutorial中对坦克的基本操作已经有了详尽的描述(其实是我懒..),因此本文着重于面板系统、服务端基本网络框架和客户端基本网络框架的搭建。
  • 如有疑问或者描述错误的地方,欢迎各位提出或修正,讨论交流是获取和巩固知识的重要途径之一!

一、坦克的基本控制
 坦克类游戏中,可以根据需要对坦克进行结构划分。书例中将坦克划分为炮管、炮塔、车身、轮子、履带。
 旋转炮管、炮塔的方法与fps类游戏的操控方法类似;
 坦克的操控可以使用基本的移动逻辑去实现,但为了更好地模拟现实中的坦克的移动方式,书中选择了WheelCollider,WheelCollider是Unity3d内置的碰撞器,能够很好地模拟现实中汽车的轮子的碰撞过程、汽车的悬挂系统、引擎系统等汽车关键的物理特性。
Unity3d-Wheel Collider 官方文档传送门
 轮子的旋转可以根据WheelCollider的旋转速度定义;
 履带可以根据轮子的旋转速度,通过改变贴图纹理的offset值实现履带滚动效果。

二、射击逻辑
 涉及实例化炮弹Prefab、炮弹飞翔轨迹(LineRender实现,可参考官方案例。。今年U3D的案例更新真的很爆肝…)、炮弹爆炸效果、Tank焚烧效果、以及相关音效。
 书中描述到的还有一个准心:设计思想为玩家通过鼠标确定Tank炮台的最终指向,但Tank本身并不会瞬移到鼠标指向的方向,而是会缓慢移动到目标方向。但由于书中是通过Physics.Raycast来检测Tank每一帧下瞄准的物体以及使用NGUI来绘制准心,因此会出现发射炮弹后,准心跟着炮弹走的现象。

三、AI坦克
 分层有限状态机是游戏实现人工智能的传统方法。神经网络也能实现坦克AI化(YouTube上有相关的视频:结合Tank Tutorial以及神经网络训练Tank AI)。
 简单来说,分层有限状态机就是给Tank定义几种状态(巡逻、进攻、逃跑等,可用枚举类实现),当达到一定条件时更改Tank的状态,Tank根据当前的状态执行不同的逻辑。
  例;
  AI默认状态为巡逻(在一个定义的范围内随机移动,看起来像在散步hhh);
  当发现敌人时,将AI的状态设置为Attack;
   实时检测以自身为中心半径范围内是否出现带有Tank标签的Tank物体;或
   给AI添加Sphere Collider当发生碰撞时检测是否为敌人;或
   使用Physics.Raycast检测正前方,这样的Tank更像瞎子…等等)
  由于每一帧都在检测AI的状态,因此一旦发生改变,AI的行为就会发生相应的改变。

注意:这种方式的AI需要自定义AI巡逻的空间,而且通常需要进行路线计算。Unity3d内置的NavMesh为我们解决了这样的问题。NavMesh可以实现自动寻路、绕开障碍物、自动寻路过程中跳过障碍物等等这些都是可以配置和实现的。
Unity3d-Navigation 官方文档传送门

前面啰嗦那么多(还偷懒了很多_,-),精彩部分即将开始!

2018-03-16 15:16:05 MadBam_boo 阅读数 941

Unity3d实战之Unity3d网络游戏实战篇(6):服务端框架的搭建

学习书籍《Unity3d网络游戏实战》 罗培羽著 机械工业出版社
本文是作者在学习过程中遇到的认为值得记录的点,因此引用的代码等资源基本出资罗培羽老师的书籍
以下为个人的学习简记,与诸君共论 由于U3D的官方案例Tank Tutorial中对坦克的基本操作已经有了详尽的描述,因此本文着重于面板系统、服务端基本网络框架和客户端基本网络框架的搭建
如有疑问或者描述错误的地方,欢迎各位提出或修正,讨论交流是获取和巩固知识的重要途径之一!

 整个服务端框架分为3层:
这里写图片描述
 底层:负责Client的监听、数据处理、协议构建、数据库操作等。
 中间层:对客观世界事物的抽象,实现类对象的各种方法;
 逻辑层:实现游戏逻辑,根据接收到的不同协议实现不同的逻辑。

 服务端框架的目录结构如下:
 这里写图片描述
 底层:
  ServNet:处理Server与各Client的连接以及数据接收;
  DataMgr:根据需求实现对数据库的各种操作;
  Protocol:编写协议实现Server与Client之间的有效通信;
 中间层:
  Player:对玩家的抽象,拥有玩家id、角色、下线等方法的实现;
  Conn:实现Server与Client之间的连接的管理,其作用与第四节描述的一致;
 逻辑层:
  HandleConnMsg:处理连接相关的事务,如心跳机制、用户登陆登出等;
  HandlePlayerMsg:处理玩家相关的事务,如位置同步、得分获取等;
  HandlePlayerEvent:处理玩家事件,某个时间发生时要处理的事情,如玩家登陆登出等;
  PlayerData:指定需要保存到数据库的角色属性,如金币、等级、胜负数等;
  PlayerTempData:指定玩家角色的临时属性,如是否在房间内、是否在战斗中、是否已准备等;

2018-03-23 20:07:47 MadBam_boo 阅读数 632

Unity3d实战之Unity3d网络游戏实战篇(13):登录&注册面板

学习书籍《Unity3d网络游戏实战》 罗培羽著 机械工业出版社
本文是作者在学习过程中遇到的认为值得记录的点,因此引用的代码等资源基本出资罗培羽老师的书籍,如有侵权请联系,必删。

 建立好网络模块后,我们可以开始对UI进行编程,实现可视化的登录注册面板。在第二节-代码资源分离的界面系统中,实现了Panel基类,之后的所有面板都会基于该基类并根据需求进行实现。本节给出登录、注册的实例。

 1、登录面板
 根据自己的喜好构建一个登录界面,这里给出一个测试样例:
 这里写图片描述
 注意,其中的Panel和Tips是Canvas下的空物体,用于对各种Panel进行分组,他们拥有的是Rect Transform组件而不是Transform组件。
 LoginPanel本身是Panel类型,以Text为后缀的是Text类型,以Input为后缀的是IntputField类型,以Btn为后缀的是Button类型。记得将LoginPanel拖拽到Resources文件夹中,因为PanelMgr的OpenPanel方法将从Resources文件夹中寻到对应的Panel并实例化。

 根据LoginPanel所示,我们在登录时需要输入用户名、密码、点击登录或者注册。这四个物体使我们所关注的。
 创建C# Script,命名为LoginPanel,声明四个变量:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;   // don't forget it.

public class LoginPanel : PanelBase {
    /*
     * Add a logout listener.
     */
    private InputField idInput;  // InputField of user name 
    private InputField pwInput;  // InputField of password
    private Button loginBtn;     // Button of login
    private Button regBtn;       // Button of register
}

 然后,按照需求重载父类PanelBase的Init()方法和OnShowing()方法:

#region Operation-Cycle
public override void Init (params object[] args)
{
    base.Init (args);
    skinPath = "LoginPanel";
    layer = PanelMgr.PanelLayer.Panel;
}

public override void OnShowing ()
{
    base.OnShowing ();
    Transform skinTrans = skin.transform;
    idInput = skinTrans.Find ("idInput").GetComponent<InputField> ();
    pwInput = skinTrans.Find ("pwInput").GetComponent<InputField> ();
    loginBtn = skinTrans.Find ("loginBtn").GetComponent<Button> ();
    regBtn = skinTrans.Find ("regBtn").GetComponent<Button> ();

    loginBtn.onClick.AddListener (OnLoginButtonClick);
    regBtn.onClick.AddListener (OnRegisterButtonClick);
}
#endregion

 在Init()方法中,将skinPath设置为LoginPanel,这样调用PanelMgr.OpenPanel方法才能在Resource文件夹中找到对应的Panel。
 在OnShowing()方法中,首先,获取到LoginPanel的实例的Transform组件,然后使用Transform.Find(string name).GetComponent<>()来获取需要用到的组件。并给Button添加点击事件。

 当用户点击Login按钮时,Client需要检测用户输入的用户名和密码是否为空,如果为空则输出对应的提示信息(Tips组的Panel,实现方法与LoginPanel的方法相似,读者可尝试自行实现);否则,将用户名和密码打包成消息发送给服务端,并等待服务端的处理结果,根据处理结果决定用户是进入游戏大厅还是输出提示信息。
 当用户点击Register按钮时,跳转到RegisterPanel,关闭LoginPanel。

#region OnButtonClickEvent
void OnLoginButtonClick()
{
    if (idInput.text == "" || pwInput.text == "") {
        //Debug.Log ("[LoginPanel.OnLoginButtonClick] Id and pw can't stay empty.");
        PanelMgr.instance.OpenPanel<TipPanel>("","Id and pw can't stay empty.");
        return;
    }

    if (NetMgr.servConn.status != Connection.Status.Connected) {
        NetMgr.servConn.proto = new ProtocolBytes ();
        if (!NetMgr.servConn.Connect ("127.0.0.1", 1234)) {
            PanelMgr.instance.OpenPanel<TipPanel> ("", "Connect server fail.");
        }
    }

    ProtocolBytes protocol = new ProtocolBytes ();
    protocol.AddString ("Login");
    protocol.AddString (idInput.text);
    protocol.AddString (pwInput.text);
    Debug.Log ("[LoginPanel.OnLoginButtonClick] Send protocol: Login " + idInput.text + "[id] " + pwInput.text + "[pw]");
    NetMgr.servConn.Send (protocol, OnLoginBack);
}

void OnRegisterButtonClick()
{
    PanelMgr.instance.OpenPanel<RegisterPanel> ("");
    Close ();
}
#endregion

 在Register的点击事件中,我们先对用户输入进行判空处理,然后对用户的网络连接进行检测,之后再打包用户输入的用户名和密码,发送给服务端并等待处理结果。在事件结尾我们使用了NetMgr.servConn.Send (protocol, OnLoginBack);,(详情传送门走起)当接收到服务端返回的处理结果后,Client将调用OnLoginBack方法,根据结果执行不同的逻辑。

#region OnBackEvent
void OnLoginBack(ProtocolBase protoBase)
{
    ProtocolBytes protocolRec = (ProtocolBytes)protoBase;
    int start = 0;
    string protoName = protocolRec.GetString (start, ref start);
    int result = protocolRec.GetInt (start, ref start);
    if (result == 0) {
        //Debug.Log ("[LoginPanel.OnLoginBack] Login success. ");
        PanelMgr.instance.OpenPanel<TipPanel>("","Login success. ");
        PanelMgr.instance.OpenPanel<LobbyPanel> ("");
        Close ();
    }else{
        //Debug.Log ("[LoginPanel.OnLoginBack] Login fail. ");
        PanelMgr.instance.OpenPanel<TipPanel>("","Login fail. Please check your username and password.");
    }
}
#endregion

 如果用户密码正确,进入大厅,否则弹出错误提示。

 2、注册面板
 测试样例:
 这里写图片描述
 需要用到的UI跟LoginPanel相差不大,只是多了一个确认密码的UI而已。其他部分改改Text、改改位置即可。
 在RegisterPanel中,增加了一个Repaet UI,用于用户密码的二次确认。其他部分跟LoginPanel类似,因此,RegisterPanel脚本跟LoginPanel脚本不同的地方就在于对用户的输入的密码增加一个二次确认的判断,其余部分相差不大,读者可自行体会:
 

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

public class RegisterPanel : PanelBase {

    private InputField idInput;
    private InputField pwInput;
    private InputField rpInput;
    private Button regBtn;
    private Button closeBtn;

    #region Operation-Cycle
    public override void Init (params object[] args)
    {
        base.Init (args);
        skinPath = "RegisterPanel";
        layer = PanelMgr.PanelLayer.Panel;
    }

    public override void OnShowing ()
    {
        base.OnShowing ();
        Transform skinTrans = skin.transform;
        idInput = skinTrans.Find ("idInput").GetComponent<InputField> ();
        pwInput = skinTrans.Find ("pwInput").GetComponent<InputField> ();
        rpwInput = skinTrans.Find ("rpInput").GetComponent<InputField> ();
        regBtn = skinTrans.Find ("regBtn").GetComponent<Button> ();
        closeBtn = skinTrans.Find ("closeBtn").GetComponent<Button> ();

        regBtn.onClick.AddListener (OnRegisterButtonClick);
        closeBtn.onClick.AddListener (OnCloseButtonClick);
    }
    #endregion

    #region OnButtonClickEvent
    void OnRegisterButtonClick()
    {
        // check empty input.
        if (idInput.text == "" || pwInput.text == "" || rpInput.text == "") {
            //Debug.Log ("[RegisterPanel.OnRegisterButtonClick] Id and pw and check pw can't stay empty.");
            PanelMgr.instance.OpenPanel<TipPanel>("","Id and pw and check pw can't stay empty.");
            return;
        }

        // check if the password correct.
        if (pwInput.text != rpInput.text) {
            //Debug.Log ("[RegisterPanel.OnRegisterButtonClick] Two password are different.");
            PanelMgr.instance.OpenPanel<TipPanel>("","Two password are different.");
            return;
        }

        // check the connection status
        if (NetMgr.servConn.status != Connection.Status.Connected) {
            NetMgr.servConn.clientProtocol= new ProtocolBytes ();
            if (!NetMgr.servConn.Connect ("127.0.0.1", 1234)) {
                PanelMgr.instance.OpenPanel<TipPanel> ("", "Connect server fail.");
            }
        }

        // send message
        ProtocolBytes protocol = new ProtocolBytes ();
        protocol.AddString ("Register");
        protocol.AddString (idInput.text);
        protocol.AddString (pwInput.text);
        Debug.Log ("[LoginPanel.OnRegisterButtonClick] Send protocol: Register " + idInput.text + "[id] " + pwInput.text + "[pw]");
        NetMgr.servConn.Send (protocol, OnRegisterBack);     // wait for result.
    }

    void OnCloseButtonClick()
    {
        PanelMgr.instance.OpenPanel<LoginPanel> ("");
        Close ();
    }
    #endregion

    #region OnBackEvent
    void OnRegisterBack(ProtocolBase protoBase)
    {
        ProtocolBytes protocol = (ProtocolBytes)protoBase;
        int start = 0;
        string protoName = protocol.GetString (start, ref start);
        int result = protocol.GetInt (start, ref start);
        //  if ok, open login panel, else print out tips panel.
        if (result == 0) {
            //Debug.Log ("[RegisterPanel.OnRegisterBack] Register success.");
            PanelMgr.instance.OpenPanel<TipPanel>("","Register success.");
            PanelMgr.instance.OpenPanel<LoginPanel> ("");
            Close ();
        } else {
            //Debug.Log ("[RegisterPanel.OnRegisterBack] Register fail.");
            PanelMgr.instance.OpenPanel<TipPanel>("","Register fail. Please use another id.");
        }
    }
    #endregion
}

 本节只举了LoginPanel和RegisterPanel两个例子,其他的面板实现方式都是类似的,如果有认真读懂这两个脚本的话,自己设计其他面板是没有太大的问题的。如果想要UI好看点,可以研究一下Unity3d的Aniimation。
 官方教程传送门-Unity3d Animation Tutorial

如果想要在打开面板前后加入动画或者其他逻辑,可以重载OnShowing和OnShowed方法;
如果想要在关闭面板前后加入动画或者其他逻辑,可以重载OnClosing和OnClosed方法。

 以上。

2018-03-16 15:58:00 MadBam_boo 阅读数 874

Unity3d实战之Unity3d网络游戏实战篇(7):数据管理类DataMgr

学习书籍《Unity3d网络游戏实战》 罗培羽著 机械工业出版社
本文是作者在学习过程中遇到的认为值得记录的点,因此引用的代码等资源基本出资罗培羽老师的书籍,如有侵权请联系,必删。

 DataMgr是封装数据库操作的类,它实现了用户名密码验证、注册、创建角色、获取角色数据、保存角色数据。
 
 Register:向user表插入user_name 和 password;
 CreatePlayer:创建默认PlayerData并存入player中;
 CheckPassWord:检查user_name 和 password对能否在user表中找到对应数据;
 GetPlayerData:根据user_name在player找出对应的data;
 SavePlayer:根据user_name更新player中的data;

 辅助用的方法:
 private bool IsSafeStr(string str);
 通常,我们注册一个账号、创建一个新角色并给他输入昵称时会看到有一些字符是不允许输入的,这是为了防止sql注入,所谓sql注入就是用户在用户名栏或者密码栏中恶意输入sql命令,由于程序是通过用户输入来进行数据库查询的,因此很有可能会发生数据库被破坏的情况。例如:
 用户注册了用户名为”xiaoming; delete * from player;”的名字,该字符串传到程序中会执行如下数据库操作命令:
 select * from player where id=xiaoming; delete * from player;
 瞬间爆炸!所以一定要对用户的输入进行特殊字符的排除!

/// <summary>
/// Check the string , if it has illegal characters.
/// </summary>
/// <returns><c>true</c> if this instance is safe string the specified str; otherwise, <c>false</c>.</returns>
/// <param name="str">string</param>
bool IsSafeStr(string str)
{
    return !Regex.IsMatch (str, @"[-|;|,|\/|\(|\)|\[|\]|\}|\{|%|@|\*|!|\']");
}

 private bool canRegister(string id, string pw);
 用于检测用户输入的用户名和密码是否已注册,实现方法如下:

/// <summary>
        /// Check the password.
        /// </summary>
        /// <returns><c>true</c>, if password was checked, <c>false</c> otherwise.</returns>
        /// <param name="id">user_name</param>
        /// <param name="pw">password</param>
        public bool CheckPassword(string id, string pw)
        {
            if (!IsSafeStr (id) || !IsSafeStr (pw)) {
                Console.WriteLine ("[DataMgr.CheckPassword] Illegal characters.");
                return false;
            }

            string formatStr = "select * from user where id='{0}' and pw='{1}';";
            string cmdStr = string.Format (formatStr, id, pw);
            MySqlCommand cmd = new MySqlCommand (cmdStr, sqlConn);

            try {
                MySqlDataReader dataReader = cmd.ExecuteReader();
                bool hasRow = dataReader.HasRows;
                dataReader.Close();
                return hasRow;
            } catch (Exception ex) {
                Console.WriteLine ("[DataMgr.CheckPassword] Read data fail. " + ex.Message);
                return false;
            }
        }

 余下的Register、CreatePlayer等方法的实现都是使用不同的SQL语句来实现查询、插入、读取数据等操作。编写代码的过程中要注意代码风格的一致性以及异常捕获后输出的提示信息要清晰明了,防止后面Debug的时候找不着北…血与泪的教训。
 给出两个Sample:
 

/// <summary>
/// Check the password.
/// </summary>
/// <returns><c>true</c>, if password was checked, <c>false</c> otherwise.</returns>
/// <param name="id">user_name</param>
/// <param name="pw">password</param>
public bool CheckPassword(string id, string pw)
{
    if (!IsSafeStr (id) || !IsSafeStr (pw)) {
        Console.WriteLine ("[DataMgr.CheckPassword] Illegal characters.");
        return false;
    }

    string formatStr = "select * from user where id='{0}' and pw='{1}';";
    string cmdStr = string.Format (formatStr, id, pw);
    MySqlCommand cmd = new MySqlCommand (cmdStr, sqlConn);

    try {
        MySqlDataReader dataReader = cmd.ExecuteReader();
        bool hasRow = dataReader.HasRows;
        dataReader.Close();
        return hasRow;
    } catch (Exception ex) {
        Console.WriteLine ("[DataMgr.CheckPassword] Read data fail. " + ex.Message);
        return false;
    }
}
/// <summary>
/// Create a playerData for player whose id is args' id.
/// </summary>
/// <returns><c>true</c>create player success, <c>false</c>can't create player</returns>
/// <param name="id">user_name</param>
public bool CreatePlayer(string id)
{
    if (!IsSafeStr (id)) {
        Console.WriteLine ("[DataMgr.CreatePlayer] Illegal characters.");
        return false;
    }

    PlayerData playerData = new PlayerData ();
    MemoryStream stream = new MemoryStream ();
    BinaryFormatter formatter = new BinaryFormatter ();

    try {
        formatter.Serialize(stream, playerData);
    } catch (Exception ex) {
        Console.WriteLine ("[DataMgr.CreatePlayer] Serialize data fail. " + ex.Message);
        return false;
    }

    byte[] dataBytes = stream.ToArray ();

    string formatStr = "insert into player set id='{0}', data=@data;";
    string cmdStr = string.Format (formatStr, id);
    MySqlCommand cmd = new MySqlCommand (cmdStr, sqlConn);
    cmd.Parameters.Add ("@data", MySqlDbType.Blob);
    cmd.Parameters [0].Value = dataBytes;

    try {
        cmd.ExecuteNonQuery();
        return true;
    } catch (Exception ex) {
        Console.WriteLine ("[DataMgr.CreatePlayer] Execute command fail. " + ex.Message);
        return false;
    }
}
没有更多推荐了,返回首页