此部分可以先建立游戏大厅,然后进入游戏,此处坑甚多耗费大量时间。国内百度出来的基本没靠谱的,一些专栏作家大V也不过是基本翻译了一下用户手册(坑啊),只能通过看youtube视频以及不停的翻阅用户手册解决(很多demo根本没踩到这些坑)。废话不说了,开始(此部分是在上一篇基础上开始的,一些上一篇解释过的操作就不再重复),建议看完此部分后可以看一下下一篇,有助于理解此部分的代码:

1)自定义NetworkLobbyManager

自定义脚本LobbyManager并重写NetworkLobbyManager的虚方法,惯例脚本在文末。

2)挂载脚本LobbyManager

挂载脚本是有讲究的,此脚本继承自NetworkLobbyManager。NetworkLobbyManager需要两个场景,即lobby场景和游戏场景,前者obby场景为准备场景(可以是像王者荣耀开始前队友列表UI,可以像吃鸡游戏开始之前的小岛场景),后者game场景即为真实游戏进行的场景。同时,对应的 需要两个参数lobby Player和gamePlayer,lobbyPlayer为进入lobby的游戏物体,game Player为真是游戏时的游戏物体。NetworkLobbyManager根据lobby场景中的lobbyPlayer在游戏场景中生成gamePlayer,但是当加载游戏场景后如果lobbyPlayer被销毁,则无法生成gamePlayer,所以加载场景时不能销毁。 而NetworkLobbyManager继承自NetworkManager,所以有DontDestroyOnLoad选项(自定义脚本不需要再继续添加DontDestroyOnLoad选项。而且在某些非常态lobby场景重新加载时(稍后会说到一些可能的重新加载场景)会有多个NetworkLobbyManager),所以生成的lobby Player最好是NetworkLobbyManager的子游戏物体。

3)游戏列表

游戏列表其实为了把unityService中建立的游戏进行列表,可供玩家选择加入。不同的游戏是通过LobbyMatch脚本控制,在有些列表中把每一个LobbyMatch实例化出来

4)Player定义

gamePlayer与上一篇(其实是本系列第一篇)中NetworkManager的game Player定义相同,必须包含NetworkIdentity和NetworkTransform(如果需要),在增加其他诸如移动,血量控制的脚本。lobbyPlayer则必须继承自Network LobbyPlayer。本篇通过自定义的LobbyPlayer(继承自Network LobbyPlayer)以及Lobby PlayerUI(lobby场景中LobbyPlayer为ui形态),两者脚本在文末。

PS:通过对NetworkLobbyManager应用发现很多坑,所幸基本坑都填上,坑实在太多,但是大部分均在代码中注释清楚,更加详细解释单独在下篇文章中解释。

//———————————————————————脚本————————————————————————//

using UnityEngine.Networking;
using UnityEngine.Networking.Match;
using System;
using System.Collections.Generic;
using UnityEngine; public class LobbyManager : NetworkLobbyManager
{
public static LobbyManager theLobbyManager; #region OUTER ACITONS
public Action<MatchInfo> matchCreatedAction; public Action<List<MatchInfoSnapshot>> matchListedAction; public Action destroyMatchAction; public Action dropConnAction; public Action clientSceneChangedAction;
#endregion #region HOST CALLBACKS
public override void OnStartHost()
{
LogInfo.theLogger.Log("Host create"); base.OnStartHost();
} public override void OnStopHost()
{
LogInfo.theLogger.Log("Host stop"); base.OnStopHost();
} public override void OnStopClient()
{
LogInfo.theLogger.Log("Host client"); base.OnStopClient();
} public override void OnStartClient(NetworkClient lobbyClient)
{
LogInfo.theLogger.Log("Client create"); base.OnStartClient(lobbyClient);
} public override GameObject OnLobbyServerCreateLobbyPlayer(NetworkConnection conn, short playerControllerId)
{
int num = ;
foreach (var p in lobbySlots)
{
if (p != null)
{
num++;
}
} LogInfo.theLogger.Log("ServerCreateLobbyPlayer:" + num); return base.OnLobbyServerCreateLobbyPlayer(conn, playerControllerId); ;
} public override GameObject OnLobbyServerCreateGamePlayer(NetworkConnection conn, short playerControllerId)
{
LogInfo.theLogger.Log("ServerCreateGamePlayer:" + lobbySlots.Length); return base.OnLobbyServerCreateGamePlayer(conn, playerControllerId);
} public override void OnLobbyServerPlayerRemoved(NetworkConnection conn, short playerControllerId)
{
LogInfo.theLogger.Log("LobbyServerPlayerRemoved"); base.OnLobbyServerPlayerRemoved(conn, playerControllerId);
} public override void OnServerRemovePlayer(NetworkConnection conn, PlayerController player)
{
LogInfo.theLogger.Log("ServerRemovePlayer"); base.OnServerRemovePlayer(conn, player);
} public override void OnLobbyClientSceneChanged(NetworkConnection conn)
{
LogInfo.theLogger.Log("LobbyClientSceneChanged"); base.OnLobbyClientSceneChanged(conn);
} public override void OnLobbyServerPlayersReady()//当所有当前加入的player均准备后调用此方法,但加入的玩家数不一定定于maxPlayer
{
LogInfo.theLogger.Log("LobbyServerPlayersReady:"+lobbySlots.Length); //base方法当所有加入的player均准备后进入游戏场景
//base.OnLobbyServerPlayersReady(); int readyPlayersNum = ; foreach(var player in lobbySlots)
{
if (player != null && player.readyToBegin) readyPlayersNum++;
} if (readyPlayersNum == maxPlayers) BeginGame();
}
#endregion #region MATCH CALLBACKS
public override void OnMatchCreate(bool success, string extendedInfo, MatchInfo matchInfo)
{
LogInfo.theLogger.Log("Match create"); base.OnMatchCreate(success, extendedInfo, matchInfo); if (matchCreatedAction != null)
{
matchCreatedAction(matchInfo);
}
} public override void OnMatchJoined(bool success, string extendedInfo, MatchInfo matchInfo)
{
LogInfo.theLogger.Log("Match joined_" + matchInfo.address); Debug.Log("Before callback Manager==null:" + (theLobbyManager == null)); base.OnMatchJoined(success, extendedInfo, matchInfo); Debug.Log("After callback Manager==null:" + (theLobbyManager == null));
} public override void OnMatchList(bool success, string extendedInfo, List<MatchInfoSnapshot> matchList)
{
base.OnMatchList(success, extendedInfo, matchList); LogInfo.theLogger.Log("Match list:" + matchList.Count); if (matchListedAction != null)
{
matchListedAction(matchList);
}
} public override void OnDestroyMatch(bool success, string extendedInfo)
{
LogInfo.theLogger.Log("DestroyMatch"); base.OnDestroyMatch(success, extendedInfo); StopMatchMaker();
StopHost();//Local Connection already exists if (destroyMatchAction != null)
{
destroyMatchAction();
}
} public override void OnDropConnection(bool success, string extendedInfo)
{
LogInfo.theLogger.Log("DropConnection"); base.OnDropConnection(success, extendedInfo); if (dropConnAction != null)
{
dropConnAction();
}
}
#endregion #region CLIENT CALLBACKS
public override void OnClientConnect(NetworkConnection conn)
{
LogInfo.theLogger.Log("Client connect"); base.OnClientConnect(conn);
LogInfo.theLogger.Log("ClientScene players:"+ClientScene.localPlayers.Count); conn.RegisterHandler(LobbyPlayer.playerMsg, HandlePlayMsg);
} public override void OnClientDisconnect(NetworkConnection conn)
{
LogInfo.theLogger.Log("Client disconnect"); base.OnClientDisconnect(conn);
} public override void OnClientSceneChanged(NetworkConnection conn)
{
LogInfo.theLogger.Log("ClientSceneChanged");
Debug.Log("ClientSceneChanged"); if(clientSceneChangedAction!=null)
{
clientSceneChangedAction();
}
//base中回调执行ClientScene.AddPlayer(conn, 0, msg);
//A connection has already been set as ready.There can only be one.
//UnityEngine.Networking.NetworkLobbyManager:OnClientSceneChanged(NetworkConnection)
//base.OnClientSceneChanged(conn); //if (string.IsNullOrEmpty(this.onlineScene) || this.onlineScene == this.offlineScene)
//{
// ClientScene.Ready(conn);
// if (this.autoCreatePlayer)
// {
// ClientScene.AddPlayer(conn, 0, new PlayerMsg());
// }
//}
} //class PlayerMsg : MessageBase { } #endregion private void HandlePlayMsg(NetworkMessage netMsg)
{
netMsg.conn.Disconnect();
} private void BeginGame()
{
LogInfo.theLogger.Log("开始");
ServerChangeScene(playScene);
}
//private void Awake()
//{
//DontDestroyOnLoad(gameObject);
//theLobbyManager = this;
//} //private void Update()
//{
//Debug.Log("this==null:" + (this == null));
//if(theLobbyManager==null)
//{
// theLobbyManager = this;
// Debug.Log("theManager Reset");
//}
//}
}
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking.Match;
using UnityEngine.UI; public class LobbyMainMenu : MonoBehaviour
{
public LobbyManager lobbyManager;
public Button createMatchBtn;
public Button playersBackToMain;
public Button serversBackToMain;
public Button listServerBtn;
public InputField matchName;
public GameObject gameListPanel;
public GameObject serverListPanel;
public GameObject lobbyMatch; public static LobbyMainMenu mainMenu; public void AddPlayer(LobbyPlayer player)
{
gameListPanel.SetActive(true);
serverListPanel.SetActive(false);
player.transform.SetParent(gameListPanel.transform.Find("PlayerListPanel"));
} public void DropConnection()
{
//lobbyManager = LobbyManager.theLobbyManager;
Debug.Log("Before drop_MM_" + (lobbyManager == null) + "_MK_" + (lobbyManager.matchMaker == null)); if (lobbyManager.matchMaker == null) lobbyManager.StartMatchMaker();//此语句为了测试增加,此处可以去掉 lobbyManager.matchMaker.DropConnection(lobbyManager.matchInfo.networkId,
lobbyManager.matchInfo.nodeId, , lobbyManager.OnDropConnection); Debug.Log("After drop_MM_" + (lobbyManager == null) + "_MK_" + (lobbyManager.matchMaker == null));
} public void DestroyMatch()
{
//lobbyManager.matchMaker.DropConnection(lobbyManager.matchInfo.networkId,
// lobbyManager.matchInfo.nodeId, 0, lobbyManager.OnDropConnection); lobbyManager.matchMaker.DestroyMatch(lobbyManager.matchInfo.networkId, ,
lobbyManager.OnDestroyMatch);
} private void CreateMatch()
{
if (lobbyManager.matchMaker == null) lobbyManager.StartMatchMaker(); lobbyManager.matchMaker.CreateMatch(matchName.text, , true, "", "", "", , ,
lobbyManager.OnMatchCreate);
} private void ListServers()
{
if (lobbyManager.matchMaker == null) lobbyManager.StartMatchMaker(); lobbyManager.matchMaker.ListMatches(, , "", true, , ,
lobbyManager.OnMatchList);
} private void PlayerListBackToMainMenu()
{
//lobbyManager.matchMaker.DestroyMatch(lobbyManager.matchInfo.networkId, 0,
//lobbyManager.OnDestroyMatch); //lobbyManager.matchMaker.DropConnection(lobbyManager.matchInfo.networkId,
//lobbyManager.matchInfo.nodeId, 0, lobbyManager.OnDestroyMatch);
gameListPanel.SetActive(false);
gameObject.SetActive(true);
} private void ServerListBackToMainMenu()
{
serverListPanel.SetActive(false);
gameObject.SetActive(true);
} private void OnMatchCreate(MatchInfo obj)
{
gameListPanel.SetActive(true);
gameObject.SetActive(false);
} private void OnMatchList(List<MatchInfoSnapshot> matchList)
{
foreach(Transform tt in serverListPanel.transform.Find("ServerListPanel"))
{
Destroy(tt.gameObject);
} foreach(var match in matchList)
{
GameObject matchGo = Instantiate(lobbyMatch, serverListPanel.transform.Find("ServerListPanel"));
matchGo.GetComponent<LobbyMatch>().Populate(match, lobbyManager);
} serverListPanel.SetActive(true);
gameObject.SetActive(false);
} private void OnDestroyMatch()
{
gameListPanel.SetActive(false);
gameObject.SetActive(true);
} private void OnClientSceneChanged()
{
//加载游戏场景后,将主场景ui关掉
//后续此部分还要处理,返回时还需重新打开ui
gameObject.SetActive(false);
gameListPanel.SetActive(false);
} private void Start()
{
mainMenu = this;
//lobbyManager = LobbyManager.theLobbyManager; createMatchBtn.onClick.AddListener(CreateMatch);
listServerBtn.onClick.AddListener(ListServers);
playersBackToMain.onClick.AddListener(PlayerListBackToMainMenu);
serversBackToMain.onClick.AddListener(ServerListBackToMainMenu); lobbyManager.matchCreatedAction += OnMatchCreate;
lobbyManager.matchListedAction += OnMatchList;
lobbyManager.destroyMatchAction += OnDestroyMatch;
} }
using System;
using UnityEngine;
using UnityEngine.Networking; /// <summary>
/// isLocalPayer,isServer,isClient区别:
/// OnClientEnterLobby回调时只是数据层面已经客户端连接进来,但是还没有建立client的gameobject,所以此时isLocalPlayer并未赋值(默认false)
/// 三者的区别从定义以及测试得知如下:
/// isServer:在服务端的活动的player,此值均为true,即不管是server端StartHost时自己建立的client还是Server Spawn产生,只要在服务端显示,此值均为true
/// isClient:由Server端spawn产生,并作为client运行的,此值均为true,即只要是Spawn产生的,不管是在服务端还是在客户端,此值均为true
/// 所以,服务端存在的player,isServer和isClient均为true,客户端isclient均为true,isServer均为false。(isServer与isClient并没有对立关系)
/// isLocalPayer在执行OnStartLocalPlayer回调时赋值为true,即生成本地的player时赋值,且当Server端spawn的client加入到场景中时只执行OnClientEnterLobby回调,
/// 不执行OnStartLocalPlayer回调,它的作用是识别玩家控制的游戏物体
/// </summary>
public class LobbyPlayer : NetworkLobbyPlayer
{
public Action<bool> clientEnterAction;
public Action<bool,bool> localPlayerAction;
public Action<bool> clientReadyAction; public static short playerMsg = MsgType.Highest + ; #region METHODS
public void RemovePlayer_()
{
if (isLocalPlayer)
{
//如果调用RemovePlayer以及ClientScene的RemovePlayer时只会删除服务端与客户端的player
//而客户端与服务端的NetworkConnection不会销毁(RemovePlayer亲测,ClientScene的RemovePlayer未测试,只是看释义)
//所以当离开游戏继续加入游戏时会报Error:A connection has already set as ready....
//因为每次重新连接时,connection是肯定存在,没有被销毁,所以不确定后续的风险
//所以此处调用matchMaker.DropConnection
//但是用DropConnection方法会引发Error:ClientDisconnected due to error: Timeout(不确定是否为本身bug)
//但是此问题与Error:ServerDisconnected due to error: Timeout(本身bug,有说此bug已修复,但是作者2017版本依然存在,但为下载补丁(下载补丁也许可以))相似
//此问题可控,不会有后续风险,所以在此采用后者,但不论哪一种方法,只要忽略Error,均可以运行。
//RemovePlayer();
if(isServer)
{
RemovePlayer();//先移除player,否则Error:ClientScene::AddPlayer: playerControllerId of 0 already in use.
//connectionToClient.Disconnect();
//connectionToClient.Dispose();
LobbyMainMenu.mainMenu.DestroyMatch();
Debug.Log("destroy match");
}
else
{
LobbyMainMenu.mainMenu.DropConnection();
Debug.Log("drop match");
}
}
else if(isServer)//此处是根据connectionToClient定义做的判断,去掉if(isServer)判断也可以,因为在初始化时已经做了限制,非isserver的不会调用
{
//采用Send方法是为了验证消息注册的用法(现在方法简洁明了)
//每一个player中的connection(connectionToClient)为客户端连接到主机时建立的
//connectionToClient.Send(playerMsg, new PlayerMsg());
//connectionToClient为服务端与客户端具有相同identity的player的NetworkConnection,即通过一个玩家,客户端与服务端的连接,并且只在server端有效
//所以只要断开连接即可
connectionToClient.Disconnect();
connectionToClient.Dispose();
Debug.Log("disconnect match");
}
} public void SendReadyToBeginMessage_()
{
SendReadyToBeginMessage();
} #endregion #region CALLBACKS
public override void OnClientEnterLobby()
{
base.OnClientEnterLobby();
LobbyMainMenu.mainMenu.AddPlayer(this); if(clientEnterAction!=null)
{
clientEnterAction(isServer);
} //LogInfo.theLogger.Log("Client enter lobby_"+isLocalPlayer+"_"+isServer);
LogInfo.theLogger.Log("ClientEnterLobby_"+isClient+"_"+isServer);
} public override void OnStartLocalPlayer()
{
LogInfo.theLogger.Log("StartLocalPlayer_" + isLocalPlayer + "_" + isServer); if(localPlayerAction!=null)
{
localPlayerAction(isLocalPlayer,isServer);
} base.OnStartLocalPlayer();
} public override void OnClientExitLobby()
{
base.OnClientExitLobby();
} public override void OnClientReady(bool readyState)
{
LogInfo.theLogger.Log("OnClientReady_" + readyState); base.OnClientReady(readyState); if(clientReadyAction!=null)
{
clientReadyAction(readyState);
}
}
#endregion class PlayerMsg : MessageBase { }
}
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI; public class LobbyPlayerUI : NetworkBehaviour
{
public LobbyPlayer thePlayer;
public Button removeBtn;
public Toggle readyBtn;
public GameObject readyStateImage; private void OnRemove()
{
thePlayer.RemovePlayer_();
} private void OnReady(bool isReady)
{
if (isReady)
{
thePlayer.SendReadyToBeginMessage();
}
else
{
thePlayer.SendNotReadyToBeginMessage();
}
} private void OnClientReady(bool isReady)
{
RpcOnClientReady(isReady);
} [ClientRpc]
private void RpcOnClientReady(bool isReady)
{
readyStateImage.SetActive(isReady);
} private void OnClientEnter(bool isServer)
{
if(!isServer)
{
//这段是为了对不同的客户端进行权限操作,此处代码完全可以作为初始值,不用设置
removeBtn.GetComponentInChildren<Text>().text = "Leave";
removeBtn.interactable = false;
readyBtn.interactable = false;
}
else
{
readyBtn.interactable = false;
removeBtn.GetComponentInChildren<Text>().text = "Kick"; //通过isLocalPlayer进行主机客户端进行单独设置,但是此时islocalplayer==false(未初始化)
//if (isLocalPlayer)
//{
// removeBtn.GetComponentInChildren<Text>().text = "Leave";
// readyBtn.interactable = true;
//}
}
} private void LocalPlayerIdentity(bool isLocalPlayer, bool isServer)
{
if(isLocalPlayer)//此处可以不用判断,因为当create local player时才会调用此方法,而create local player时isLocalPlayer肯定为true
{
removeBtn.interactable = true;
readyBtn.interactable = true;
GetComponent<Image>().color = Color.red; if(isServer) removeBtn.GetComponentInChildren<Text>().text = "Leave";
}
} private void Awake()
{
removeBtn.onClick.AddListener(OnRemove);
readyBtn.onValueChanged.AddListener(OnReady); thePlayer.clientEnterAction += OnClientEnter;
thePlayer.localPlayerAction += LocalPlayerIdentity;
thePlayer.clientReadyAction += OnClientReady;
}
}
using UnityEngine;

public class PlayerList : MonoBehaviour
{
public static PlayerList theList; public void AddPlayer(LobbyPlayer player)
{
player.transform.SetParent(transform);
} private void Start()
{
theList = this;
}
}

NetworkManager网络通讯_NetworkLobbyManager(三)的更多相关文章

  1. java基础54 网络通讯的三要素及网络/网页编程的概述

    1.概述 网络编程注意解决的是计算机(手机.平板.....)之间的数据传输问题.        网络编程:不需要基于html基础上,就可以进行数据间的传输.比如:FeiQ.QQ.微信.....     ...

  2. NetworkManager网络通讯_破产版NetworkManager(五)

    根据对NetWorkServer 以及NetworkClient的理解,编写一个简易版的NetWork Manager.惯例全部代码放在最后 (一)NetWorkServer与NetworkClien ...

  3. NetworkManager网络通讯_问题汇总(四)

    此篇来填坑,有些坑是unet自身问题,而大部分则是理解不准确造成的(或者unity定义太复杂) 问题一: isLocalPlayer 值一直是false 出现场景:NetworkLobbyPlayer ...

  4. NetworkManager网络通讯_NetworkManager(二)

    本文主要来实现一下自定UI(实现HUD的功能),并对Network Manger进行深入的讲解. 1)自定义manager 创建脚本CustomerUnetManger,并继承自NetworkMang ...

  5. NetworkManager网络通讯_Example(一)

    ---恢复内容开始--- 用户手册,范例精讲. 用户手册上给出了一个简单的范例,并指出可以以此为基础进行相开发,再次对范例进行精讲.(NetworkManager对使用unity的轻量级游戏开发有很大 ...

  6. NetworkManager网络通讯_networkReader/Writer(六)

    unet客户端和服务端进行消息发送时可以采用上一节中方法,也可以直接用networkReader/Writer类进行发送 (一)服务端/客户端注册消息 ; m_Server.RegisterHandl ...

  7. 网络--三种网络通讯方式及Android的网络通讯机制

    Android平台有三种网络接口可以使用,他们分别是:java.net.*(标准Java接口).Org.apache接口和Android.net.*(Android网络接口).下面分别介绍这些接口的功 ...

  8. dicom网络通讯入门(3)

    接下来可以进行消息传递了 ,也就是dimse ,再来复习下 什么是dimse .n-set  n-create c-echo 这些都是dimse  他们都是属于一种结构的pdu 那就是tf-pdu(传 ...

  9. 《连载 | 物联网框架ServerSuperIO教程》-4.如开发一套设备驱动,同时支持串口和网络通讯。附:将来支持Windows 10 IOT

    1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架Serve ...

随机推荐

  1. JAVASE知识点总结(二)

    第十三章:多态  一.instanceof 判断一个类是否是指定的类 真则返回true 假则返回false.  二.字段没有多态,只有方法有多态,字段前面是的什么类型,字段就调用谁的,在编译时就已经确 ...

  2. abp(net core)+easyui+efcore实现仓储管理系统——EasyUI之货物管理二 (二十)

    abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+ ...

  3. Java 提取Word中的文本和图片

    本文将介绍通过Java来提取或读取Word文档中文本和图片的方法.这里提取文本和图片包括同时提取文档正文当中以及页眉.页脚中的的文本和图片. 使用工具:Free Spire.Doc for Java ...

  4. 难题解决:Mycat数据库中间件+Mybatis批量插入数据并返回行记录的所有主键ID

     一.mybatis的版本必须为3.3.1及其以上 项目所依赖的mybatis的版本必须为3.3.1及其以上,低版本的不行,保证hap项目的依赖的mybatis的jar的版本必需为需要的版本: 二.在 ...

  5. java程序猿如何练习java版的易筋经?

    故事背景 电视剧<天龙八部>中,阿朱易容后进入少林寺偷走了<易筋经>,她一直想把这本书送给乔峰.耿直的乔峰觉得此书来历不正,不肯接受.几番波折,这本书最后落到聚贤庄庄主游坦之手 ...

  6. python process

    原文:https://www.cnblogs.com/LY-C/p/9145729.html 使用process模块可以创建进程 from multiprocessing import Process ...

  7. java中的左移运算符<<

    System.out.println(3<<4);//48 相当于3乘以2的4次方 将一个数左移n位,就相当于乘以了2的n次方 位运算cpu直接支持的,效率最高

  8. ES6 —— entries(),keys()和values()

    ES6 提供三个新的方法 —— entries(),keys()和values() —— 用于遍历数组.它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的 ...

  9. sql获取各种时间格式的方法

    ),)--月/日/年 ),)--年.月.日 (常用) ),)--日/月/年 ),)--日.月.年 ),)--日-月-年 ),)--日 月 年

  10. CS184.1X 计算机图形学导论 罗德里格斯公式推导

    罗德里格斯公式推导 图1(复制自wiki) 按照教程里,以图1为例子,设k为旋转轴,v为原始向量. v以k为旋转轴旋转,旋转角度为θ,旋转后的向量为vrot. 首先我们对v进行分解,分解成一个平行于k ...