Photon——Hello World Part 2 第二部分

 

Hello World Part 2 第二部分

     In   part 1  we introduced some basic concepts of the client API:   PhotonPeer,   Service,   Connect, and the listener/callback design. Building on the application of part 1 (initial connection setup) we will have a look at how to use this connection to create a simple chat, where the application joins a room and sends a “Hello World!” message to the other connected clients.
     在第1部分我们介绍了一些客户端API的基本概念: PhotonPeer,服务,连接和侦听器/回调的设计。建立在应用程序的第1部分基础上(初始连接设置)我们将看看如何使用这个连接来创建一个简单的聊天,应用程序加入一个房间,发送一个“Hello World ! “消息给其他连接的客户端。
 
Content 内容
  • Overview  概述
  • Preparation: Refactoring the Main Flow 准备:重构主要的流程
  • Operations and Events 操作和事件
  • OnStatusChanged OnStatusChanged事件
  • OnOperationResponse OnOperationResponse事件
  • OnEvent OnEvent事件
  • Final Demo Code 最后的演示代码

Overview 概述

     We are going to look into two sets of concepts:
     Operations are remote procedure calls with a request and a response. An operation request consists of an OperationCode and Parameters (a Dictionary<byte, object\> containing the parameters). To send operation requests to the server OpCustom of the peer instance is called. The operation result is returned to your application in the OnOperationResponse callback.
     Note:a) Sending the request and receiving the response are decoupled and fully asynchronous. b)the operation response is optional.
     Events are messages or notifications pushed to the client. When the peer receives an event the application is notified through the OnEvent callback
     Rooms group client connections or peers and facilitate the communication between peers. Peers can join a room and send events to all peers of the room or parts of them.OpJoin is the operation called by the client to join the room.
OpRaiseEvent is the operation called to send events to other clients.
     Note: Rooms,   OpJoin  and   OpRaiseEvent  are implemented in the   Lite Application. The room concept is essential to many games because this is the approach many developers take - although it's not the only one - Photon is committed to an   open architecture  philosophy.
     
     我们先看看两个概念:
     操作是一个远程过程调用,由一个请求和一个响应组成。一个操作请求包含一个OperationCode 和 参数(一个字典<字节,对象\ >包含了参数)。 操作请求发送到服务器peer实例的OpCustom被调用。OnOperationResponse回调时操作的结果被返回给你的应用程序中。
     注意:a)发送请求和接收响应是解耦和完全异步的。b)操作响应是可选的。
     事件是消息或通知被推送到客户端。当peer收到一个事件时,应用程序将通过OnEvent回调得到通知。 
     房间组织客户端的连接或peer并方便peer之间的交流。peer可以加入一个房间和发送事件到房间内的所有peer或其中部分。客户端加入房间时OpJoin操作被调用,将事件发送给其他客户端OpRaiseEvent操作被调用。
     注意:在Lite应用程序中实现房间,OpJoin和OpRaiseEvent。这房间的概念是至关重要的许多游戏,因为这是很多开发人员采取的方法——尽管它不是只有一个——Photon致力于一个开放式体系结构的理论研究。

Preparation: Refactoring the Main Flow 准备:重构主要流程

     We will start a new C#/Windows/Console project, name it Helloworld2, add a reference to PhotonDotNet.dll and copy the Program.cs of part 1:
     我们将开始一个新的c# / Windows /控制台项目,命名它为Helloworld2,添加一个引用PhotonDotNet.dll和复制第1部分的Program.cs:

     Next we’ll refactor the code in main as follows

  • We will move the core flow into the “Program” instance method Run (line 13).
  • Leaving a small stub of main just to create the instance and call Run (line 3).
  • Peer is declared as an instance variable and initialized in the constructor of “Program” (line 6).
     接下来我们将重构代码如下:
  • 我们将把核心流程加入到“Program”实例的Run方法(第13行)。
  • 留下一个Main的存根只是为了创建实例和调用Run (第3行)。
  • Peer是在“Program”的构造函数里声明为一个实例变量并初始化(第6行)。   
 
static   void   Main( string [] args)
{
new   Program().Run();
}
PhotonPeer peer;
public   Program()
{
peer =   new   PhotonPeer( this , ConnectionProtocol.Udp);
}
void   Run()
{
if   (peer.Connect( "localhost:5055" ,   "Lite" ))
{
do
{
//Console.WriteLine(".");
peer.Service();
System.Threading.Thread.Sleep(500);
}
while   (!Console.KeyAvailable);
}
else
Console.WriteLine( "Unknown hostname!" );
Console.WriteLine( "Press any key to end program!" );
Console.ReadKey();
}
     Note: in line 19 we commented the "." output. Optionally you can replace it by Debug.Write("."); to make sure your loop is working. To see this check the output-tab in your Visual Studio.
     注意:在第19行我们评论“.“的输出。选择你可以取代它通过调试写(“.”),确保你的循环工作。看到这检查你在Visual Studio的输出选项卡。

Operations and Events 操作和事件

     Now we are done with the preparation work we can start to see how we send an operation request to the server.
     现在我们已经完成了准备工作,我们可以开始看看我们是如何将一个操作发送请求到服务器。
 
     The flow we are implementing once we initialized the connection:
    • When status changes to connected: send the operation join request to join the room “MyRoomName”. (in OnStatusChanged)
    • When receiving the result “join ok”: send the operation raise event request with the event code 101 and a “Hello World” message. (in OnOperationResponse)
     当我们初始化连接时流程就会被实施:
    • 当状态更改为连接:发送操作加入请求,加入房间”MyRoomName”。 (在 OnStatusChanged )
    • 当接收到结果为“join ok”:发送操作触发事件的请求,事件代码101和一个“Hello World”消息。 (在 OnOperationResponse )
     Note: usually you would define event codes centrally for your application in an enumeration for instance. Because we are using Lite features the codes 255-251 ( Join,   Leave,   SetProperties,   GetProperties) are predefined.
     注意:通常你会定义事件代码集在应用程序的枚举实例中。例如, 因为我们使用Lite特性编码255 - 251(Join, Leave, SetProperties, GetProperties)是预定义的。
    • When receiving an event with the code 101: print the received message to the console. (in OnEvent)
    • 当收到一个事件代码101:打印接收到的消息到控制台里。 (在 OnEvent )

OnStatusChanged OnStatusChanged事件

     To start with the implementation of the flow we described above the first change to the code we’ll be to add a switch for the statusCode we get notified with and a case block for the StatusCode.Connect to send an OperationRequest to join a room with the name “MyRoomName” (lines 6-11):

  • where we create a hashtable opParams containing the parameters (lines 8+9).
  • and call peer.OpCustom with the OperationCode and passing in the opParams Dictionary<byte, object\> (line 10).
     首先实现上面描述的流程,我们的第一个要变化的代码是我们将添加一个当做开关的statusCode,我们得到的通知是StatusCode.Connect块发送一个OperationRequest加入一个叫做“MyRoomName”的房间 (行6-11):
  • 我们创建一个哈希表包含参数opParams (第8行+ 9)。
  • 和调用peer.OpCustom 与 OperationCode 并传递这个opParams字典<字节,对象\ >(第10行)。
 
public   void   OnStatusChanged(StatusCode statusCode)
{
Console.WriteLine( "\n---OnStatusChanged:"   + statusCode);
switch   (statusCode)
{
case   StatusCode.Connect:
Console.WriteLine( "Calling OpJoin ..." );
Dictionary<Byte, Object>opParams =   new   Dictionary<Byte,Object>();
opParams[( byte )LiteOpKey.GameId] =   "MyRoomName" ;
peer.OpCustom(( byte )LiteOpCode.Join, opParams,   true );
break ;
default :
break ;
}
}
  

OnOperationResponse OnOperationResponse事件

     For an easy way to print out readable operation codes we will define the following enum:
     对于一个简单的方法来打印可读的操作码,我们将定义为以下的枚举:
 
 
enum   OpCodeEnum :   byte
{
Join = 255,
Leave = 254,
RaiseEvent = 253,
SetProperties = 252,
GetProperties = 251
}
 
     Next we change the   OnOperationResponse  callback as follows:
    1. We check the operationResponse.ReturnCode if it failed (not 0) we exit the method (lines 3,8).
    2. Add a switch-case for OperationCode (operationResponse.OperationCodeLiteOpCode.Join (lines 11,13)
    3. We create a RaiseEvent operation request (LiteOpCode.RaiseEvent). With the parameters LiteOpKey.Code = 101 and LiteOpKey.Data = “Hello World” (our message) (lines 18-21).
     接下来,我们改变OnOperationResponse的回调如下:
  1. 我们检查 operationResponse.ReturnCode 如果它失败了(不是0) 我们退出方法(行3-8)。
  2. 添加一个开关例子为OperationCode ( operationResponse.OperationCode ) LiteOpCode.Join (行11-13)
  3. 我们创建一个RaiseEvent操作请求( LiteOpCode.RaiseEvent )。参数LiteOpKey.Code= 101和LiteOpKey.Data= " Hello World”(我们的消息)(第18 - 21行)。
 
public   void   OnOperationResponse(OperationResponse operationResponse)
{
if   (operationResponse.ReturnCode == 0)
Console.WriteLine( "\n---OnOperationResponse: OK - "   + (OpCodeEnum)operationResponse.OperationCode +   "("   + operationResponse.OperationCode +   ")" );
else
{
Console.WriteLine( "\n---OnOperationResponse: NOK - "   + (OpCodeEnum)operationResponse.OperationCode +   "("   + operationResponse.OperationCode +   ")\n ->ReturnCode="   + operationResponse.ReturnCode +   " DebugMessage="   + operationResponse.DebugMessage);
return ;
}
switch   (operationResponse.OperationCode)
{
case   ( byte )LiteOpCode.Join:
int   myActorNr = ( int )operationResponse.Parameters[LiteOpKey.ActorNr];
Console.WriteLine( " ->My PlayerNr (or ActorNr) is:"   + myActorNr);
Console.WriteLine( "Calling OpRaiseEvent ..." );
Dictionary< byte ,   object > opParams =   new   Dictionary< byte ,   object >();
opParams[LiteOpKey.Code] = ( byte )101;
opParams[LiteOpKey.Data] =   "Hello World!" ;
peer.OpCustom(( byte )LiteOpCode.RaiseEvent, opParams,   true );
break ;
}
}
 
     When you run this code the ouput will look like this:
     当您运行这个代码,输出将看起来像这样:
 
C:\...\HelloWorld2\bin\Debug>HelloWorld2.exe
---OnStatusChanged:Connect
Calling OpJoin ...
---OnOperationResponse: OK - Join(90)
->My PlayerNr (or ActorNr) is:1
Calling OpRaiseEvent ...
---OnOperationResponse: NOK - RaiseEvent(253)
->ReturnCode=-1 DebugMessage=Wrong parameter type 245
(RaiseEventRequest.Data): should be Hashtable but received String
 
     As you can see the first operation ( Join) returned   OK. It also returned the actor number 1 ( ->My PlayerNr (or ActorNr) is:1). If you start a second Helloworld2.exe you will see a “2” instead. The next client would get a “3”.
     正如您可以看到的第一个操作( 加入 )返回OK。它还返回1号的actor ( - >我的PlayerNr(或ActorNr)是:1 )。如果你开始运行第二个Helloworld2.exe,你将看到一个“2”。接下来的客户端将会得到一个“3”。
 
     Note: starting and stopping clients might lead to higher actor No. values. The server recognizes the disconnect after a certain timeout and removes those peers. Usually, a client would send a disconnect on shutdown - which is the recommended way, so others "see" faster that a peer disconnected - a peer removed from the room triggers a leave event broadcasting it to all peers connected.
     注:启动和停止客户端可能会导致更多的actor没有值。服务器断开后识别到某些超时并删除那些Peer。通常,客户端在关闭时会发送一个断开——这是被推荐的方式,所以其他人更快的“看到” peer断开连接——一个peer离开房间会触发一个离开事件并广播它给所有连接着的peer。
 
     The second operation   OpRaiseEvent  failed. This is because the parameter   Data  is expected to be a hashtable. So we will change our code as follows (lines 10-13):
     第二个操作OpRaiseEvent失败了。这是因为参数数据预计将是一个散列表。所以我们将改变我们的代码去遵循(第10行):
 
 
switch   (operationResponse.OperationCode)
{
case   LiteOpCode.Join:
int   myActorNr = ( int )operationResponse.Parameters[LiteOpKey.ActorNr];
Console.WriteLine( " ->My PlayerNr (or ActorNr) is:"   + myActorNr);
Console.WriteLine( "Calling OpRaiseEvent ..." );
Dictionary< byte ,   object > opParams =   new   Dictionary< byte ,   object >();
opParams[LiteOpKey.Code] = ( byte )101;
//opParams[LiteOpKey.Data] = "Hello World!"; //<- returns an error, server expects a hashtable
Hashtable evData =   new   Hashtable();
evData[( byte )1] =   "Hello Wolrd!" ;
opParams[LiteOpKey.Data] = evData;
peer.OpCustom(( byte )LiteOpCode.RaiseEvent, opParams,   true );
break ;
}
 
     When running the code with the changes we just made you will notice the   OnOperationResponse  for   RaiseEvent  doesn’t appear anymore - this happens, because the server only sends a response in case of an error!
     当运行代码发生变化时,我们只是让你注意到 OnOperationResponse 对于 RaiseEvent 不出现了——发生这一切是因为服务器在出错的情况下只发送一个响应!
 
     Note: The key (byte)1 of evData has nothing to do with the parameter opParams[LiteOpKey.Code] beeing (byte)101(line 9).
     Hint: It's recommended to use either byte or short keys in the evData hashtable, because events tend to be sent very often.
     注意:Key(字节)1的evData没有参数opParams[LiteOpKey.Code]可用(字节)101(第9行)。
     提示:这是推荐要么使用byte,要么使用短的Key在evData哈希表,这是因为事件往往会经常发送。

OnEvent OnEvent事件

     For an easy way to print out readable operation codes we will define the following Enum:
     用一个简单的方法来打印可读的操作码,我们将定义以下枚举:
 
 
enum   EvCodeEnum :   byte
{
Join = 255,
Leave = 254,
PropertiesChanged = 253
}       
 
     The last change we’re making is to display the message we receive in the eventData when we receive the event with code = 101:
     最后,当我们接收到事件eventData与code = 101的时候,在eventData里显示我们收到的消息:
 
 
public   void   OnEvent(EventData eventData)
{
Console.WriteLine( "\n---OnEvent: "   + (EvCodeEnum)eventData.Code +   "("   + eventData.Code +   ")" );
switch   (eventData.Code)
{
case   101:
int   sourceActorNr = ( int )eventData.Parameters[LiteEventKey.ActorNr];
Hashtable evData = (Hashtable)eventData.Parameters[LiteEventKey.Data];
Console.WriteLine( " ->Player"   + sourceActorNr +   " say's: "   + evData[( byte )1]);
break ;
}
}
 
     If you launch two clients you’ll now see the following:
     如果您启动两个客户你现在将看到如下:

     the first client: 第一个客户端

C:\...\HelloWorld2\bin\Debug>HelloWorld2.exe
---OnStatusChanged:Connect
Calling OpJoin ...
---OnOperationResponse: OK - Join(255)
->My PlayerNr (or ActorNr) is:1
Calling OpRaiseEvent ...
---OnEvent: Join(255)
->Player1 joined!
->Total num players in room:1, Actornr List: 1,
---OnEvent: Join(255)
->Player2 joined!
->Total num players in room:2, Actornr List: 1,2,
---OnEvent: 101(101)
->Player2 say's: Hello Wolrd!

     the second client: 第二个客户端

C:\...\HelloWorld2\bin\Debug>HelloWorld2.exe
---OnStatusChanged:Connect
Calling OpJoin ...
---OnOperationResponse: OK - Join(255)
->My PlayerNr (or ActorNr) is:2
Calling OpRaiseEvent ...
---OnEvent: Join(255)
->Player2 joined!
->Total num players in room:2, Actornr List: 1,2,
 
     The first client receives 3 events:
    1. the join event triggered by its own join.
    2. the join event triggered by the joining of the second client.
    3. the 101 event the second client sent to all others in the room
     第一个客户端接收3个事件:
  1. join事件被自己的加入所触发。
  2. join事件被第二个加入的客户端所触发。
  3. 101事件,第二个客户端发送给房间里的所有其他人。
      all others  is the default behavior of   OpRaisEvent  (for more details see SDK documentation of LitePeer).The second client only receives one join event (triggered by its own join).
     所有其他人的事件触发是OpRaisEvent的默认行为 (更多细节 看到SDK文档的LitePeer)。第二个客户端只收到一个加入事件(被自己的加入触发)。
 
     In this tutorial, we only used   PhotonPeer. The client SDK also includes a   LitePeer  to ease the use of Lite features. It extends the PhotonPeer, so when joining a room with LitePeer you only need to call   peer.OpJoin(“MyRoomName”). To see how this works just replace PhotonPeer by LitePeer. Another powerful Lite feature worth looking into is   Properties. In Lite you can set room and player (or actor) properties. As an example you could set initial configuration values (e.g. the game map, difficulty, …) as room properties or the players nicknames as actor properties (for more details see the client SDK documentation).
     在本教程中,我们只使用了PhotonPeer。客户端SDK还包括了一个LitePeer为了更方便的使用Lite。它扩展了PhotonPeer,所以当LitePeer加入一个房间,你只需要调用peer.OpJoin (“MyRoomName”)。接下来看看PhotonPeer接替LitePeer是如何工作的。另一个强大的Lite特性值得研究的是属性。在Lite可以设置房间和玩家的属性。作为一个示例,您可以设定初始配置值(例如游戏地图,困难,…)作为房间属性或玩家昵称作为玩家的属性(详情见客户端SDK文档)。

Final Demo Code 最后的演示代码

     In the code that follows we’ve added a couple of lines to increase debug-output, which should help you to dig deeper into the details of how Photon and Lite work.
     下列代码所示,我们已经添加了一对调试输出,这应该有助于您更深入的理解Photon和Lite的工作细节。
 
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
using ExitGames.Client.Photon;
using ExitGames.Client.Photon.Lite;
using System.Diagnostics;
using System.Collections;
 
namespace HelloWorld2
{
     class Program : IPhotonPeerListener
     {
         static void Main( string [] args)
         {
             new Program().Run();
         }
 
         PhotonPeer peer;
 
         public Program()
         {
             peer = new PhotonPeer( this , ConnectionProtocol.Udp);
         }
 
         void Run()
         {
             //DebugLevel should usally be ERROR or Warning - ALL lets you "see" more details of what the sdk is doing.
             //Output is passed to you in the DebugReturn callback
             peer.DebugOut = DebugLevel.ALL;
             if (peer.Connect( "localhost:5055" , "Lite" ))
             {
                 do
                 {
                     Debug.Write( "." ); //allows you to "see" the game loop is working, check your output-tab when running from within VS
                     peer.Service();
                     System.Threading.Thread.Sleep(50);
                 }
                 while (!Console.KeyAvailable);
             }
             else
                 Console.WriteLine( "Unknown hostname!" );
             Console.WriteLine( "Press any key to end program!" );
             Console.ReadKey();
             //peer.Disconnect(); //<- uncomment this line to see a faster disconnect/leave on the other clients.
         }
 
         #region IPhotonPeerListener Members
         public void DebugReturn(DebugLevel level, string message)
         {
             // level of detail depends on the setting of peer.DebugOut
             Debug.WriteLine( "\nDebugReturn:" + message); //check your output-tab when running from within VS
         }
 
         public void OnOperationResponse(OperationResponse operationResponse)
         {
             if (operationResponse.ReturnCode == 0)
                 Console.WriteLine( "\n---OnOperationResponse: OK - " + (OpCodeEnum)operationResponse.OperationCode + "(" + operationResponse.OperationCode + ")" );
             else
             {
                 Console.WriteLine( "\n---OnOperationResponse: NOK - " + (OpCodeEnum)operationResponse.OperationCode + "(" + operationResponse.OperationCode + ")\n ->ReturnCode=" + operationResponse.ReturnCode
                   + " DebugMessage=" + operationResponse.DebugMessage);
                 return ;
             }
 
             switch (operationResponse.OperationCode)
             {
                 case LiteOpCode.Join:
                     int myActorNr = ( int )operationResponse.Parameters[LiteOpKey.ActorNr];
                     Console.WriteLine( " ->My PlayerNr (or ActorNr) is:" + myActorNr);
 
                     Console.WriteLine( "Calling OpRaiseEvent ..." );
                     Dictionary< byte , object > opParams = new Dictionary< byte , object >();
                     opParams[LiteOpKey.Code] = ( byte )101;
                     //opParams[LiteOpKey.Data] = "Hello World!"; //<- returns an error, server expects a hashtable
 
                     Hashtable evData = new Hashtable();
                     evData[( byte )1] = "Hello Wolrd!" ;
                     opParams[LiteOpKey.Data] = evData;
                     peer.OpCustom(( byte )LiteOpCode.RaiseEvent, opParams, true );
                     break ;
             }
         }
 
         public void OnStatusChanged(StatusCode statusCode)
         {
             Console.WriteLine( "\n---OnStatusChanged:" + statusCode);
             switch (statusCode)
             {
                 case StatusCode.Connect:
                     Console.WriteLine( "Calling OpJoin ..." );
                     Dictionary< byte , object > opParams = new Dictionary< byte , object >();
                     opParams[LiteOpKey.GameId] = "MyRoomName" ;
                     peer.OpCustom(( byte )LiteOpCode.Join, opParams, true );
                     break ;
                 default :
                     break ;
             }
         }
 
         public void OnEvent(EventData eventData)
         {
             Console.WriteLine( "\n---OnEvent: " + (EvCodeEnum)eventData.Code + "(" + eventData.Code + ")" );
 
             switch (eventData.Code)
             {
                 case LiteEventCode.Join:
                     int actorNrJoined = ( int )eventData.Parameters[LiteEventKey.ActorNr];
                     Console.WriteLine( " ->Player" + actorNrJoined + " joined!" );
 
                     int [] actorList = ( int [])eventData.Parameters[LiteEventKey.ActorList];
                     Console.Write( " ->Total num players in room:" + actorList.Length + ", Actornr List: " );
                     foreach ( int actorNr in actorList)
                     {
                         Console.Write(actorNr + "," );
                     }
                     Console.WriteLine( "" );
                     break ;
 
                 case 101:
                     int sourceActorNr = ( int )eventData.Parameters[LiteEventKey.ActorNr];
                     Hashtable evData = (Hashtable)eventData.Parameters[LiteEventKey.Data];
                     Console.WriteLine( " ->Player" + sourceActorNr + " say's: " + evData[( byte )1]);
                     break ;
             }
         }
 
         #endregion
     }
 
     enum OpCodeEnum : byte
     {
         Join = 255,
         Leave = 254,
         RaiseEvent = 253,
         SetProperties = 252,
         GetProperties = 251
     }
 
     enum EvCodeEnum : byte
     {
         Join = 255,
         Leave = 254,
         PropertiesChanged = 253
     }
}

转载于:https://www.cnblogs.com/liusuqi/archive/2013/05/15/3079702.html

Photon Pun2是Unity Engine官方提供的网络通信插件,它主要用于游戏中的实时多人在线同步。帧同步是指所有玩家在同一时刻看到的游戏画面保持一致。在Pun2中实现帧同步,可以采用以下步骤: 1. **启用帧同步**: 在`PhotonNetwork`组件上,打开`UseMasterClient`选项,这会让一个玩家成为“主客户端”,负责控制帧率,其他客户端则跟随。 ```csharp PhotonNetwork.useMasterClient = true; ``` 2. **设置网络更新频率**: 客户端需要按照同步周期发送位置更新消息。比如,你可以选择每秒发送一次位置,而不是每次移动都发送。 ```csharp float updateInterval = 1f / NetworkUpdateRate; // NetworkUpdateRate可以根据实际网络状况调整 ``` 3. **定时发送位置更新**: 使用`PhotonView`的`OnPhotonSerializeView`方法,只在指定时间间隔更新位置,并忽略非关键帧的变化。 ```csharp void FixedUpdate() { if (isMine && Time.time - lastUpdate > updateInterval) { PhotonView(viewId).RPC("SendPosition", RPCMode.SendToAllButOwner, new Vector3(transform.position)); lastUpdate = Time.time; } } ``` 4. **服务器处理同步请求**: 主服务器会在接收到客户端的位置更新后广播给其他客户端,确保它们同步显示。 5. **优化策略**: 如果网络延迟很高,你还可以考虑增加预测(Predictive Movement),即先预估下一帧的位置,在收到服务器确认后再调整。 ```csharp private Vector3 predictedPosition; void OnPredictionError(Vector3 actualPosition, float timeSinceLastReceived) { // 这里处理预测误差 } // 接收服务器响应后的更新 public void SendPosition(Vector3 newPosition) { transform.position = predictedPosition + (newPosition - predictedPosition) * smoothness; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值