
U3D引擎基础知识
U3D引擎基础知识
★★ 请描述游戏动画有哪几种及其原理
游戏动画是游戏中实现角色、物体或场景动态变化的技术,能使游戏世界更加生动和互动。游戏动画通常有几种不同的类型,它们的实现原理各有不同。
★★ 矩阵相乘的意义及注意点
矩阵相乘是线性代数中非常重要的一项操作,广泛应用于计算机图形学、物理模拟、机器学习等领域。
矩阵相乘的意义:
矩阵相乘的注意点:
★ 向量的点乘、叉乘以及归一化的意义
向量的点乘:
其中, 是两个向量之间的夹角, 和 分别是向量 和 的模(大小)。
在 3D 空间中,点乘公式也可以表示为:
其中, 和 是两个向量的分量。
向量的叉乘:
其中, 和 ,结果向量 。
也可以表示为:
其中, 是垂直于 和 的单位向量。
向量的归一化:
其中, 是单位向量, 是向量 的长度(模)。
★ StringBuilder 类型比 String 类型的优势是什么
在 C# 中,StringBuilder 和 String 都用于表示字符串,但它们在性能和使用场景上有一些重要的区别。
特性 | String | StringBuilder |
---|---|---|
不可变性 | 不可变,修改时会创建新的字符串实例 | 可变,直接在内部字符数组上修改 |
性能 | 多次修改会导致性能下降,尤其是在循环中 | 高效,适合大量字符串操作 |
内存管理 | 每次操作都会分配新的内存并复制数据 | 使用内部数组,减少内存分配和复制的开销 |
线程安全性 | 线程安全 | 非线程安全,多个线程操作时需要额外处理 |
适用场景 | 字符串不常修改的场景 | 需要频繁修改或连接字符串的场景 |
★ 请简述值类型与引用类型的区别
在 C# 中,值类型和值类型和引用类型有很大的区别,它们在内存中的存储方式、生命周期、性能等方面有显著的差异。
特性 | 值类型 (Value Type) | 引用类型 (Reference Type) |
---|---|---|
存储位置 | 存储在栈上或结构体内直接存储数据 | 存储在堆上,栈上存储引用 |
赋值操作 | 赋值时会复制实际的值,修改不会影响其他变量 | 赋值时复制引用(内存地址),修改会影响所有引用的变量 |
内存管理 | 由栈自动管理,生命周期较短 | 由垃圾回收器管理,生命周期较长 |
类型示例 | int、char、bool、struct | class、string、array、delegate |
存储数据 | 存储实际的数据 | 存储数据的引用 |
性能 | 小型数据,访问速度较快 | 大型数据,访问速度较慢 |
★ WebRequest 使用详解
WebResponse:响应的基类,表示请求的响应内容。
HttpWebResponse:WebResponse 的子类,处理 HTTP 请求的响应。
WebRequest:请求的基类,负责发送请求并接收响应。
HttpWebRequest:WebRequest 的一个常用子类,专用于 HTTP 请求,允许设置 HTTP 请求的详细信息(如请求头、方法等)。
WebRequest request = WebRequest.Create("http://example.com");
根据 URL 的协议类型,Create 方法会自动返回不同的请求类型。
WebResponse response = request.GetResponse();
用于发送请求并获取响应。
Stream dataStream = request.GetRequestStream();
获取一个请求流,用于发送数据(通常是 POST 请求)。
// 举个 GET 请求的例子:
using System;
using System.IO;
using System.Net;
using System.Text;
class Program
{
static void Main()
{
// 创建一个 HttpWebRequest 实例
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://example.com");
// 设置请求方法 (GET 或 POST)
request.Method = "GET";
// 设置请求头 (例如 User-Agent)
request.UserAgent = "MyUserAgent";
// 获取响应
try
{
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
// 获取响应流
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
// 读取响应内容
string responseText = reader.ReadToEnd();
Console.WriteLine(responseText);
}
}
}
catch (WebException e)
{
if (ex.Response != null)
{
using (StreamReader reader = new StreamReader(ex.Response.GetResponseStream()))
{
string errorResponse = reader.ReadToEnd();
Console.WriteLine("Error Response: " + errorResponse);
}
}
else
{
Console.WriteLine("Error: " + ex.Message);
}
Console.WriteLine($"Error: {e.Message}");
}
}
}
// 举个 POST 请求的例子:
using System;
using System.IO;
using System.Net;
using System.Text;
class Program
{
static void Main()
{
// 创建一个 HttpWebRequest 实例
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://example.com/api");
// 设置请求方法为 POST
request.Method = "POST";
// 设置请求头
request.ContentType = "application/x-www-form-urlencoded"; //(application/json 或 application/x-www-form-urlencoded)
// 请求体数据
string postData = "name=John&age=30";
// 获取请求流,并写入数据
byte[] byteArray = Encoding.UTF8.GetBytes(postData);
request.ContentLength = byteArray.Length;
using (Stream dataStream = request.GetRequestStream())
{
dataStream.Write(byteArray, 0, byteArray.Length);
}
// 获取响应
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
string responseText = reader.ReadToEnd();
Console.WriteLine(responseText);
}
}
}
}
// 举个异步响应例子:
// HttpWebRequest 也支持异步操作,这对于需要提高性能和响应速度的应用非常重要。你可以使用 BeginGetResponse 和 EndGetResponse 来异步获取响应。
HttpWebRequest request = (HttpWebRequest)WebRequest.Create("http://example.com");
request.Method = "GET";
request.BeginGetResponse(new AsyncCallback(ResponseCallback), request);
private static void ResponseCallback(IAsyncResult result)
{
HttpWebRequest request = (HttpWebRequest)result.AsyncState;
HttpWebResponse response = (HttpWebResponse)request.EndGetResponse(result);
using (StreamReader reader = new StreamReader(response.GetResponseStream()))
{
string responseText = reader.ReadToEnd();
Console.WriteLine(responseText);
}
}
常用类和方法:
UnityWebRequest:这个类表示网络请求,封装了发送请求、接收响应以及错误处理的所有功能。
UnityWebRequest.Get:发送一个 GET 请求,用于获取数据。
UnityWebRequest.Post:发送一个 POST 请求,用于提交数据。
UnityWebRequest.SendWebRequest():发送请求并开始等待响应。
UnityWebRequest.downloadHandler:用于下载响应数据。
UnityWebRequest.uploadHandler:用于上传请求数据。
UnityWebRequest.SetRequestHeader():设置请求头。
UnityWebRequest.isNetworkError / isHttpError:检查请求是否有网络错误或 HTTP 错误。
// 举个 GET 请求例子
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class WebRequestExample : MonoBehaviour
{
// 使用协程发起请求
IEnumerator Start()
{
// 发送 GET 请求
UnityWebRequest request = UnityWebRequest.Get("http://example.com");
// 发送请求并等待响应
yield return request.SendWebRequest();
// 检查是否有错误
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log("Request failed: " + request.error);
if (request.isNetworkError)
{
Debug.Log("Network Error: " + request.error);
}
else if (request.isHttpError)
{
Debug.Log("HTTP Error: " + request.error);
}
}
else
{
// 成功,输出响应文本
Debug.Log("Response: " + request.downloadHandler.text);
}
}
}
// 举个 POST 请求的例子
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class PostRequestExample : MonoBehaviour
{
IEnumerator Start()
{
// 创建 POST 数据
WWWForm form = new WWWForm();
form.AddField("name", "John");
form.AddField("age", 30);
// 发送 POST 请求
UnityWebRequest request = UnityWebRequest.Post("http://example.com/api", form);
// 发送请求并等待响应
yield return request.SendWebRequest();
// 检查是否有错误
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log("Request failed: " + request.error);
}
else
{
// 成功,输出响应文本
Debug.Log("Response: " + request.downloadHandler.text);
}
}
}
// 举个上传文件的例子
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.IO;
public class FileUploadExample : MonoBehaviour
{
IEnumerator Start()
{
// 文件路径
string filePath = "path_to_your_file";
byte[] fileData = File.ReadAllBytes(filePath);
// 创建 PUT 请求上传文件
UnityWebRequest request = UnityWebRequest.Put("http://example.com/upload", fileData);
// 发送请求并等待响应
yield return request.SendWebRequest();
// 检查是否有错误
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log("Request failed: " + request.error);
}
else
{
// 成功,输出响应文本
Debug.Log("Response: " + request.downloadHandler.text);
}
}
}
// 举个处理 JSON 数据例子:UnityWebRequest 非常适合处理 JSON 数据,通常用于与 RESTful API 交互。你可以使用 JsonUtility 类来处理 JSON 数据的序列化和反序列化。
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
[System.Serializable]
public class Person
{
public string name;
public int age;
}
public class JsonRequestExample : MonoBehaviour
{
IEnumerator Start()
{
// 创建 JSON 对象
Person person = new Person { name = "John", age = 30 };
string json = JsonUtility.ToJson(person);
// 创建请求
UnityWebRequest request = new UnityWebRequest("http://example.com/api", "POST");
// 设置请求头为 JSON
request.SetRequestHeader("Content-Type", "application/json");
// 上传 JSON 数据
byte[] jsonData = System.Text.Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(jsonData);
request.downloadHandler = new DownloadHandlerBuffer();
// 发送请求并等待响应
yield return request.SendWebRequest();
// 检查是否有错误
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log("Request failed: " + request.error);
}
else
{
// 成功,输出响应文本
Debug.Log("Response: " + request.downloadHandler.text);
}
}
}
// 举个解析响应的 JSON 数据例子:
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
[System.Serializable]
public class ResponseData
{
public string message;
}
public class JsonResponseExample : MonoBehaviour
{
IEnumerator Start()
{
// 发送 GET 请求
UnityWebRequest request = UnityWebRequest.Get("http://example.com/api");
// 发送请求并等待响应
yield return request.SendWebRequest();
// 检查是否有错误
if (request.result != UnityWebRequest.Result.Success)
{
Debug.Log("Request failed: " + request.error);
}
else
{
// 成功,解析 JSON 响应
string jsonResponse = request.downloadHandler.text;
ResponseData responseData = JsonUtility.FromJson<ResponseData>(jsonResponse);
Debug.Log("Message: " + responseData.message);
}
}
}
★ 如何优化内存
★ class和struct的异同
相同点:
★ 什么是托管代码,什么是CLR
使用基于公共语言运行库的语言编译器开发的代码称为托管代码。托管代码具有许多优点,例如:跨语言集成、跨语言异常处理、增强的安全性、版本控制和部署支持、简化的组件交互模型、调试和分析服务等。CLR:公共语言运行库 Common Language Runtime。是一个运行时环境,它负责资源管理(内存分配和垃圾收集),并保证应用和底层操作系统之间必要的分离。
★ 简述动态加载资源的方式和区别
在 Unity 中,动态加载资源是指在运行时加载和卸载游戏资源,而不是在游戏启动时就一次性加载所有资源。动态加载资源可以有效地优化内存使用,提升游戏性能,特别是当游戏包含大量资源(如场景、纹理、模型、音效等)时。
using UnityEngine;
public class ResourcesLoadExample : MonoBehaviour
{
void Start()
{
// 动态加载资源
GameObject prefab = Resources.Load<GameObject>("Prefabs/MyPrefab");
// 实例化对象
if (prefab != null)
{
Instantiate(prefab);
}
}
}
是 Unity 提供的异步加载方法,它可以在后台加载资源,而不会阻塞主线程。适用于一些比较大的资源(如大型纹理、场景等)需要在游戏运行时动态加载。
using UnityEngine;
using System.Collections;
public class ResourcesAsyncLoadExample : MonoBehaviour
{
void Start()
{
// 异步加载资源
StartCoroutine(LoadResourceAsync("Prefabs/MyPrefab"));
}
IEnumerator LoadResourceAsync(string path)
{
ResourceRequest request = Resources.LoadAsync(path);
yield return request;
if (request.asset != null)
{
GameObject prefab = (GameObject)request.asset;
Instantiate(prefab);
}
else
{
Debug.LogError("Failed to load resource.");
}
}
}
using UnityEngine;
using System.Collections;
public class AssetBundleLoadExample : MonoBehaviour
{
IEnumerator Start()
{
// 异步加载 AssetBundle
string bundleURL = "http://www.example.com/myassetbundle";
UnityWebRequest www = UnityWebRequest.Get(bundleURL);
yield return www.SendWebRequest();
if (www.result == UnityWebRequest.Result.Success)
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(www);
if (bundle.isStreamedSceneAssetBundle)
{
// 加载场景
string[] scenePaths = bundle.GetAllScenePaths();
UnityEngine.SceneManagement.SceneManager.LoadScene(System.Array.IndexOf(scenePaths, "scene1"));
}
else
{
// 加载预设
GameObject prefab = bundle.LoadAsset<GameObject>("MyPrefab");
Instantiate(prefab);
}
}
else
{
Debug.LogError("Failed to load AssetBundle.");
}
}
}
Addressable 是 Unity 提供的现代资源管理系统,旨在解决 AssetBundle 的一些痛点。它提供了一种更高层次的 API,简化了资源的打包、加载、管理和卸载。
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
public class AddressableLoadExample : MonoBehaviour
{
public AssetReference assetReference;
void Start()
{
// 异步加载资源
AsyncOperationHandle<GameObject> handle = assetReference.LoadAssetAsync<GameObject>();
handle.Completed += OnAssetLoaded;
}
private void OnAssetLoaded(AsyncOperationHandle<GameObject> handle)
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(handle.Result);
}
else
{
Debug.LogError("Failed to load asset.");
}
}
}
AssetDatabase 是 Unity 编辑器的一个类,主要用于编辑器内部对资源进行管理和操作,包括资源导入、查找、加载、删除等操作。
// 加载一个预制件
using UnityEditor;
using UnityEngine;
public class LoadAssetExample : MonoBehaviour
{
[MenuItem("Assets/Load My Prefab")]
static void LoadMyPrefab()
{
// 加载资源
GameObject myPrefab = AssetDatabase.LoadAsset<GameObject>("Assets/Prefabs/MyPrefab.prefab");
}
}
--- --- --- --- --- --- --- --- --- ---
// 加载一个材质
using UnityEditor;
using UnityEngine;
public class LoadMaterialExample : MonoBehaviour
{
[MenuItem("Assets/Load My Material")]
static void LoadMyMaterial()
{
// 加载材质
Material myMaterial = AssetDatabase.LoadAsset<Material>("Assets/Materials/MyMaterial.mat");
}
}
--- --- --- --- --- --- --- --- --- ---
// 加载一个音频
using UnityEditor;
using UnityEngine;
public class LoadAudioClipExample : MonoBehaviour
{
[MenuItem("Assets/Load My Audio Clip")]
static void LoadMyAudioClip()
{
// 加载音频剪辑
AudioClip myClip = AssetDatabase.LoadAsset<AudioClip>("Assets/Audio/MySound.wav");
}
}
方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Resources.Load | 简单,适合小型项目 | 同步加载,可能造成卡顿 ;资源管理不灵活 | 小型项目,快速原型开发 |
Resources.LoadAsync | 异步加载,避免阻塞主线程 | 依赖 Resources 文件夹,灵活性较差 | 中型项目,场景切换或资源加载 |
AssetBundle | 高效的资源管理,支持异步加载 | 需要额外构建和管理,可能会增加复杂性 | 大型项目,资源优化和热更新 |
Addressable | 高效,支持远程加载,自动管理资源 | 配置较为复杂,学习曲线较陡峭 | 中大型项目,支持热更新和跨平台 |
AssetDatabase.LoadAsset | 大批量的操作编辑器中的资源 | 仅在编辑器中使用 | 它适用于编辑器扩展和开发工具中 |
★ 简述如何在不同的分辨率下保持UI的一致性
在 Unity 中,确保不同分辨率下 UI 的一致性是一个重要的设计问题。无论用户使用的是高清显示器、标准显示器还是手机、平板、PC等不同的设备,UI 的显示效果都需要尽量保持一致。Unity 提供了几种方法来帮助开发者在不同分辨率下保持 UI 的一致性,主要是通过 Canvas Scaler、Anchor、布局组件 等技术来适配屏幕尺寸和分辨率。
Canvas Scaler
Canvas Scaler 组件有一个 UI Scale Mode 设置,提供了三种常用的模式:
Constant Pixel Size(常量像素大小):
这种模式下,UI 元素的尺寸保持固定,不会根据屏幕分辨率改变。即使屏幕尺寸发生变化,UI 元素的大小也不会改变。这适合在不同分辨率下显示相同的像素级别内容,但可能会导致在大屏幕或小屏幕上的显示效果不理想。
Scale With Screen Size(与屏幕尺寸缩放):
这种模式下,UI 元素的大小会根据屏幕的分辨率进行缩放,以保持视觉一致性。你可以设置一个基准分辨率(如 1920x1080),然后根据当前分辨率自动调整 UI 的比例。常见的应用方式是保持 UI 元素的相对大小一致,而非绝对像素大小。
设置:
Reference Resolution:你可以设置一个参考分辨率(例如,1920x1080)。UI 将根据这个参考分辨率缩放。
Screen Match Mode:选择如何匹配不同分辨率:
Match Width or Height:UI 会根据宽度或高度来进行缩放,Match 值控制宽高比的优先级。
Expand:UI 会按比例扩大,以填满屏幕。
Shrink:UI 会按比例缩小,以适应屏幕。
Constant Physical Size(常量物理尺寸):
这种模式下,UI 元素的大小保持物理尺寸一致,不论分辨率如何,UI 会根据设备的 DPI(每英寸点数)进行缩放,使得 UI 元素的物理尺寸在不同设备上保持一致。
建议:对于大多数游戏,使用 Scale With Screen Size 模式是最常见的选择,因为它可以自动调整 UI 元素以适应不同分辨率和屏幕尺寸。
Anchors
Anchors(锚点):
Anchors 是 UI 元素的“参照点”,决定了元素相对于父容器的位置和大小。UI 元素的位置和大小会根据父容器的大小和锚点位置动态调整。例如,设置一个按钮的锚点在屏幕的左上角,当屏幕大小发生变化时,按钮会始终保持在左上角,不会偏离。
Pivot(中心点):
Pivot 是 UI 元素的中心点,用于定义 UI 元素的位置和旋转的参考点。调整 Pivot 可以确保 UI 元素的正确定位。例如,设置一个元素的 Pivot 在中心,可以保证该元素始终居中。建议 在设计 UI 布局时,尽量使用 Anchor 来动态调整元素的位置,避免硬编码绝对位置。这样,UI 会根据屏幕大小进行适应,保持一致性。
Layout Group
Layout Group 组件帮助你管理 UI 元素的布局,使得元素在容器中的位置和尺寸能根据屏幕分辨率自动调整。包括以下几种:
- Horizontal Layout Group:将子元素水平排列。
- Vertical Layout Group:将子元素垂直排列。
- Grid Layout Group:将子元素按照网格形式排列。
这些布局组件会自动根据父容器的尺寸调整子元素的大小和位置,从而确保 UI 元素能够自适应不同的屏幕分辨率和设备。
提示
NGUI 很好的解决了这一点,屏幕分辨率的自适应性,原理就是计算出屏幕的宽高比跟原来的预设的屏幕分辨率求出一个对比值,然后修改摄像机的 size。
★ 什么叫做链条关节
链条关节(Chain Joint)是一种物理模拟中的约束机制,通常用于模拟一系列物体之间的连接和相对运动。它可以模拟两个物体间用一根链条连接在一起的情况,能保持两个物体在一个固定距离内部相互移动而不产生作用力,但是达到固定距离后就会产生拉力。这个概念在物理引擎中很常见,尤其是在做机械系统、生物模型 或 复杂的物理交互时。Unity 中常用的关节类型:
链条关节的物理特性包括:
★ 请描述 Prefab 的作用,并描述如何在移动设备的环境下恰当的使用它
在 Unity 中,Prefab 是高效管理游戏对象的一个重要工具。它能够帮助开发者在场景中 复用、同步 和 优化游戏对象。
Prefab 的作用:它允许你将场景中的一个或多个物体(如 GameObject、组件、材质等)保存为一个模板,以便在项目中多次复用。Prefab 使得开发者能够高效地管理和复用游戏对象,特别是在大型项目中,使用 Prefab 可以极大提高开发效率,减少重复劳动。
在移动设备上合理使用 Prefab 需要特别注意性能优化,包括简化 Prefab 复杂度、优化贴图和材质、使用对象池管理 Prefab 实例、避免频繁的动态生成以及管理好内存使用等。通过这些方法,开发者可以确保在移动平台上使用 Prefab 时不会对性能和内存造成不必要的负担,从而提升游戏的流畅度和响应速度。
Prefab
★ 实时点光源的优缺点是什么
★ 请简述 OnBecameVisible 及 OnBecameInvisible 的发生时机,以及对这一回调函数的意义
在 Unity 中,OnBecameVisible 和 OnBecameInvisible 是两个常用于渲染器(Renderer) 组件的回调函数,它们可以帮助开发者在物体的可见性状态发生变化时执行特定操作。这两个函数在物体的摄像机视野范围内变化时被自动调用。
这两个回调函数通常用于性能优化和资源管理,特别是在大规模场景或者需要动态加载、卸载物体的情况下,它们有助于减少不必要的计算,提升游戏的运行效率。
★ 什么是导航网格
导航网格(NavMesh,Navigation Mesh) 是一种广泛用于游戏和模拟中的数据结构,用来表示游戏世界或虚拟环境中的可行走区域。它通常用于 AI 角色的路径规划和导航,使得角色能够在复杂的环境中找到从一个位置到另一个位置的最短路径。
using UnityEngine;
using UnityEngine.AI;
public class AICharacter : MonoBehaviour
{
public Transform target; // 目标位置
private NavMeshAgent agent;
void Start()
{
agent = GetComponent<NavMeshAgent>(); // 获取 NavMeshAgent 组件
}
void Update()
{
if (target != null)
{
agent.SetDestination(target.position); // 设置目标位置,AI 会根据导航网格计算路径
}
}
}
★ Lod 是什么,优缺点是什么
LOD(Level of Detail,细节层次)是计算机图形学和游戏开发中常用的一种优化技术,用于在渲染过程中根据物体与相机的距离动态地调整物体的细节级别,从而提高渲染性能。
using UnityEngine;
public class Example : MonoBehaviour
{
public LODGroup lodGroup;
void Start()
{
// 获取物体上的 LODGroup 组件
lodGroup = GetComponent<LODGroup>();
}
void Update()
{
// 根据距离动态调整 LOD 级别(这里只是一个简单示例)
float distance = Vector3.Distance(Camera.main.transform.position, transform.position);
if (distance < 10f)
{
// 切换到 LOD0(高细节)
lodGroup.SetLODSwitch(0);
}
else if (distance < 20f)
{
// 切换到 LOD1(中等细节)
lodGroup.SetLODSwitch(1);
}
else
{
// 切换到 LOD2(低细节)
lodGroup.SetLODSwitch(2);
}
}
}
★ Unity 摄像机的 Clipping Plane 的作用是什么,调整 near、far 两个值,应注意什么
在 Unity 中,Clipping Plane(裁剪平面)是摄像机的一个重要参数,用于确定摄像机能看到的物体的最小距离和最大距离。它主要由两个参数控制:Near Clipping Plane(近裁剪面)和 Far Clipping Plane(远裁剪面)。这两个裁剪平面定义了摄像机的视锥体(视野)内,物体可见的深度范围。物体位于这个范围内时,会被渲染;如果物体超出了这个范围,就会被裁剪掉。
★ 动画层的作用是什么
在 Unity 中,动画层(Animation Layers)是一个非常强大的功能,允许你将动画分成多个层次,每一层可以控制不同的动画行为。通过动画层,开发者可以实现更加复杂和灵活的动画系统,尤其是在角色动画中。例如一个角色可以在行走的同时进行攻击,或者在不同的情况下触发不同的动作。
总结:动画层在 Unity 中的作用是分离和管理不同动画行为,通过多个层次控制不同的动画部分,使得角色或物体可以同时进行多种独立的动画。使用动画层的优点包括:
★ 射线检测碰撞物的原理是什么
在 Unity 中,射线检测(Raycasting)是一种常用的物理检测方法,用于判断一条射线是否与场景中的碰撞体相交,并获取碰撞信息。Unity 的射线检测基于物理引擎(PhysX),其原理和实现方式如下:
// 最基本的射线检测方法:
Ray ray = new Ray(origin, direction);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, maxDistance))
{
Debug.Log("Hit object: " + hit.collider.name);
Debug.Log("Hit point: " + hit.point);
}
// 检测射线与所有碰撞体的交点
RaycastHit[] hits = Physics.RaycastAll(ray, maxDistance);
foreach (var hit in hits)
{
Debug.Log("Hit object: " + hit.collider.name);
}
// 检测两点之间是否有碰撞体
if (Physics.Linecast(start, end, out hit))
{
Debug.Log("Hit object: " + hit.collider.name);
}
// 从摄像机发射射线到鼠标点击位置
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 可视化射线(调试)
Debug.DrawRay(ray.origin, ray.direction * maxDistance, Color.red);
★ Render 的作用是什么?描述 MeshRender 和 SkinnedMeshRender 的关系与不同
在 Unity 中,Render 是指负责将游戏对象的几何形状渲染到屏幕上的组件。它们决定了游戏对象在场景中的外观和视觉效果。Unity 中有多种渲染组件,其中最常用的是 MeshRenderer 和 SkinnedMeshRenderer。
GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
MeshRenderer renderer = cube.GetComponent<MeshRenderer>();
renderer.material.color = Color.red; // 设置材质颜色
GameObject character = Instantiate(characterPrefab);
SkinnedMeshRenderer skinnedRenderer = character.GetComponentInChildren<SkinnedMeshRenderer>();
skinnedRenderer.material.color = Color.blue; // 设置材质颜色
特性 | MeshRenderer | SkinnedMeshRenderer |
---|---|---|
用途 | 渲染静态网格(如建筑物、道具) | 渲染动态网格(如角色、怪物) |
骨骼动画支持 | 不支持 | 支持 |
网格变形 | 不支持 | 支持 |
性能开销 | 较低 | 较高(因为需要计算骨骼动画) |
组件依赖 | 需要 Mesh Filter | 需要 Mesh 和骨骼数据 |
适用场景 | 静态物体 | 动态物体(如角色、动画模型) |
★ 简述 SkinnedMesh 的实现原理
SkinnedMesh 是 Unity 中用于实现骨骼动画(Skinning Animation)的一种技术,主要用于渲染动态变形的网格。其核心原理是通过骨骼和顶点权重来实现网格的变形。以下是 SkinnedMesh 的实现原理的简要说明:
★ 四元数是什么?主要作用什么?对欧拉角的优点是什么?
★ 简述一下对象池,你觉得 FPS 里哪些东西适合使用对象池
★ 怎么判断两个平面是否相交?不能用碰撞体,说出计算方法
判断两个平面是否相交是一个几何问题,可以通过数学方法来解决。以下是判断两个平面是否相交的计算方法:
检查法向量是否平行:如果两个平面的法向量平行,则两个平面要么重合,要么平行。
★ Override 与 Overload 的区别
★ 面向对象的三大特点
提示
封装 :隐藏对象的字段和实现细节,对外提供接口。public全局,protected子类,internal同集,private隐藏。public属性,private字段,对赋值进行限定。sealed修饰符的子类是不能被继承的。设计上分而治之,封装变化,高内聚低耦合。数据上把一些基本数据复合成一个自定义类型数据。方法上隐藏实现细节,对外提供接口。
继承:重用现有代码。
多态:静态多态重载,动态多态重写。父类行为由子类具体实现,包含virtual虚方法,abstract抽象方法,interface接口。
★ 请简述 GC 产生的原因,并描述如何避免
垃圾回收(GC)是自动内存管理的一部分,产生的原因:
GC 工作原理:
尽管垃圾回收能帮助开发者自动管理内存,但也有其带来的性能开销,尤其是在大规模应用中。以下是一些避免或减轻 GC 性能问题的方法:
总之,避免垃圾回收的关键是减少不必要的对象分配和延长对象的生命周期(静态成员、常量)。C# 的垃圾回收是一种强大的内存管理机制,它可以减轻开发人员的内存管理负担,但也需要开发人员了解其工作原理,以优化应用程序的性能和内存使用。理解垃圾回收的概念和最佳实践对于开发高质量的 C# 应用程序非常重要。
// 强引用(Strong Reference)示例:
// 强引用是最常见的引用类型,它会阻止垃圾回收器回收对象,直到引用不再存在。
class MyClass
{
public string Name { get; set; }
public MyClass(string name)
{
Name = name;
}
}
class Program
{
static void Main()
{
// 创建一个强引用
MyClass strongRef = new MyClass("强引用示例");
// 对象仍然存活,因为有强引用指向它
Console.WriteLine(strongRef.Name); // 输出 "强引用示例"
// 强引用不再存在,对象成为垃圾回收的候选
strongRef = null;
// 在此点触发垃圾回收(实际回收时机由垃圾回收器决定)
GC.Collect();
// 对象被回收,无法再访问
Console.WriteLine(strongRef.Name); // 这里会引发 NullReferenceException
}
}
// 在上面的示例中,strongRef 是一个强引用,它会一直保持对象存活,直到该引用被赋值为 null。
// 一旦强引用不再存在,垃圾回收器可以回收对象。
// 弱引用(Weak Reference)示例:
// 弱引用不会阻止垃圾回收器回收对象,对象仍然可以被回收,但你可以在需要时恢复对对象的引用。
class MyClass
{
public string Name { get; set; }
public MyClass(string name)
{
Name = name;
}
}
class Program
{
static void Main()
{
// 创建一个弱引用
WeakReference weakRef = new WeakReference(new MyClass("弱引用示例"));
// 尝试获取对象引用
if (weakRef.TryGetTarget(out MyClass target))
{
Console.WriteLine(target.Name); // 输出 "弱引用示例"
}
else
{
Console.WriteLine("对象已被回收");
}
// 强制触发垃圾回收
GC.Collect();
// 尝试再次获取对象引用
if (weakRef.TryGetTarget(out target))
{
Console.WriteLine(target.Name); // 输出 "对象已被回收"
}
else
{
Console.WriteLine("对象已被回收");
}
}
}
// 在上面的示例中,weakRef 是一个弱引用,它允许对象在没有强引用的情况下被垃圾回收。
// 你可以使用 TryGetTarget 方法来尝试获取对象的引用,
// 如果对象仍然存在,就可以访问它;否则,你会得到一个指示对象已被回收的消息。
★ 结构体和类有何区别
该题类似于【请简述值类型与引用类型的区别】这道题目。
// 结构体
struct Point
{
public int X;
public int Y;
}
Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1; // p1 的副本赋给 p2
p2.X = 30; // 修改 p2 不会影响 p1
Console.WriteLine(p1.X); // 输出 10
Console.WriteLine(p2.X); // 输出 30
// 类
class Person
{
public string Name;
public int Age;
}
Person p1 = new Person { Name = "Alice", Age = 25 };
Person p2 = p1; // p2 引用 p1
p2.Name = "Bob"; // 修改 p2 会影响 p1
Console.WriteLine(p1.Name); // 输出 Bob
Console.WriteLine(p2.Name); // 输出 Bob
★ 反射的实现原理
反射(Reflection)是指在运行时动态地检查和操作对象类型、属性、方法、字段等信息的机制。在 .NET 中,反射允许你在程序运行时访问和修改类、结构体、接口、方法、属性、字段等类型的元数据。它主要用于动态加载类型、调用方法、访问字段和属性等操作。
反射的实现原理基于 元数据(Metadata) 和 程序集(Assembly)。当程序编译时,编译器将生成一个包含元数据的程序集,反射就是通过这些元数据来访问和操作类型信息。
// 使用 typeof 关键字获取静态类型:
Type type = typeof(MyClass);
// 使用 GetType() 方法获取实例类型:
MyClass obj = new MyClass();
Type type = obj.GetType();
// 通过 Assembly 加载类型:
Assembly assembly = Assembly.Load("MyAssembly");
Type type = assembly.GetType("MyNamespace.MyClass");
// 获取字段:
FieldInfo field = type.GetField("FieldName");
// 获取属性:
PropertyInfo property = type.GetProperty("PropertyName");
// 获取方法:
MethodInfo method = type.GetMethod("MethodName");
// 创建对象实例:
object instance = Activator.CreateInstance(type);
// 调用方法:
method.Invoke(instance, new object[] { parameter1, parameter2 });
// 访问字段:
object fieldValue = field.GetValue(instance);
// 设置字段值:
field.SetValue(instance, newValue);
未尽事宜补充
Assembly 加载: 在运行时,CLR(Common Language Runtime)负责加载程序集。CLR提供了一个装载程序集的机制,允许应用程序在运行时加载程序集,这些程序集可以位于磁盘上的文件、内存中或网络上。程序集一旦加载,CLR就会将其转换为运行时对象。
Type Discovery: 一旦程序集加载,CLR 通过反射 API 允许你发现和查询程序集中的类型。你可以使用 Type 类来获取类型的信息,包括名称、方法、字段、属性等。
动态代码生成: 反射还可以用于动态生成代码。例如,你可以使用 System.Reflection.Emit 命名空间中的类来动态创建类型、方法和字段。
.net编写的程序集包含两个重要部分(IL中间语言代码)和metadata(元数据),我们编写的代码中有很多很多的类,类又有很多成员,在编译代码的时候,元数据表就根据代码把类的所有信息都记录在了它里面(其实它就是个数据结构,组织类的信息)。而反射的过程刚好相反,就是通过元数据里的记录找到该类的成员,并能使它"复活"(因为元数据里所记录的信息足够详细,以至于可以根据metadata里面记录的信息找到关于该类的IL code并加以利用)。
★ .Net 与 Mono 的关系
.net 从一个抽象上来说其实是一个理念,使得多种语言编写的程序能够通过一个通用的 runtime 运行在不同的操作系统以及硬件平台上。但光有理念不行,还需要实现,这里把对于 .net 里面的某个实现叫做 .net platform,比如 .net framwork 就是一个在 windows 上实现的 .net platform , mono 则是还可以运行在 linux,unix,macos的 .net platform。
mono 是 .net 理念的一个开源跨平台工具,mono 可以实现跨平台,可以运行于 Linux,Unix,MacOS 等。
.net framwork 类似 mono 则是对 .net 标准(这个标准具体包括 CLI,CIL,.net standard 等)在 windows 平台上的一套实现,具体上说 .net framework 包含一整套解决方案,包含许多组件。比如:编译器、CLR、FCL 等等,其中每个组件都有自己的版本。
一个.net platform 想要达成 .net 的目标,就需要一些组件,比如 CLR 通用语言运行时,FCL 基础类库,各种语言的编译器,以及编译后能在 CLR 上运行的代码需要遵循 CLI 和 CIL 规则,CIL 规定了编译输出的规则,CLI 规定了编译输入语言的规则,只有符合这种标准的语言才能编译成 CIL 语言运行在 CLR 中。
.net 提供的基础类库,方便了开发者,降低了开发成本,所以基础类库也需要一个标准,而 .net standard 就是用于这个目的,它规定了某个 .net platform 需要提供哪些 API 给开发者。这样在 .net platform A 上开发了一个项目,然后迁移到 .net platform B 上,那么只要两个 platform 实现了同一个 .net standard 那么源代码就无需修改可以直接编译运行。假如有一台机器,装了.net platform A (比如.net framework)和.net platform B(比如 mono ),那么在 A 上编译出来的一个 .net 程序放到 B 上可以运行吗?理论上应该没问题,毕竟 CIL 是统一的,虽然一个是 A 的 CLR 一个是 B 的 CLR ,但是他们都是用来处理 CIL 程序,就像 java 代码编译出来既可以运行在 JVM 上也可以运行在 delvik 上一样。然而实际上不一定,因为 CIL 本身也不是一成不变的,它也有自己的版本。
★ 堆和栈的区别
栈通常保存着我们代码执行的步骤,如 AddFive()方法,int pValue变量,int result 变量等等。而堆上存放的则多是对象,数据等。我们可以把栈想象成一个接着一个叠放在一起的盒子。当我们使用的时候,每次从最顶部取走一个盒子,当一个方法(或类型)被调用完成的时候,就从栈顶取走,接着下一个。堆则不然,像是一个仓库,储存着我们使用的各种对象等信息,跟栈不同的是他们被调用完毕不会立即被清理掉。
★ 装箱和拆箱
装箱:值类型转换成引用类型object。
拆箱:引用类型object转换成值类型。
避免装箱操作,生成新的引用,解决办法就是第一是重载,第二是泛型。
如何在 Unity3D 中查看场景的面数,顶点数和 Draw Call数?如何降低Draw Call数?
在Game视图右上角点击Stats。降低Draw Call 的技术是 Draw Call Batching。
简述水面倒影的渲染原理
原理就是对水面的贴图纹理进行扰动,以产生婆光粼粼的效果。用shader可以通过GPU在像素级别做扰动,效果细腻,需要的顶点少,速度快。
什么是 Unity,它的主要功能是什么
Unity 是一款跨平台的游戏开发引擎,它具有以下主要功能和特点:
总的来说,Unity 是一款强大的游戏开发引擎,它的主要功能是提供了一套完整的工具和技术,使开发者能够创建高质量、跨平台的游戏和交互应用程序。它的易用性和广泛社区支持使其成为游戏开发者和交互设计师的首选工具之一。
能用foreach遍历访问的对象需要实现接口或声明方法的类型
IEnumerable 和 GetEnumerator
Unity和Android与iOS如何交互
Unity可以导出 Android 和 iOS 的工程,然后通过安卓或者 iOS 的类去给 Unity 发消息,调用 Unity 中的方法。
Unity 中的 GameObject、Component 和 Prefab 有什么区别
总结:
通过组合 GameObject 和附加不同的组件,以及使用 Prefab,开发者可以构建复杂的游戏世界并轻松管理和扩展游戏对象的功能。
UNITY3d 在移动设备上的一些优化资源的方法
Unity3d 实现 2d 游戏有几种方式
如何实现游戏的暂停、加速和减速
提示
Time.timeScale=1 时,Update、LateUpdate、FixedUpdate 都正常执行。
Time.timeScale=2 时,Update 和 LateUpdate 的执行速度是之前的2倍,而 FixedUpdate 还是正常执行。
Time.timeScale 会影响到所有的音频播放,为了在暂停时保持音效,你可以使用 AudioListener.pause 来单独控制音频的暂停与恢复。
Time.timeScale 影响物理系统和动画系统,如果你需要让物理或动画系统在暂停或加速时独立运行,可以调整 Time.fixedDeltaTime 或 Animator.speed 来进行微调。
将Camera组件的ClearFlags选项选成Depth only是什么意思?有何用处?
如果把摄像机的ClearFlags勾选为Deapth Only,那么摄像机就会只渲染看得见的对象,把背景会完全透明,这种情况一般用在两个摄像机以上的场景中。
有A和B两组物体,有什么办法能够保证A组物体永远比B组物体先渲染?
通过shader里面的渲染队列来渲染,设置A组物体的渲染队列大于B物体的渲染队列。
游戏切换后台和前台调用的函数是什么
当游戏切换到后台时,Unity 会调用 OnApplicationPause 和 OnApplicationFocus 函数。
using UnityEngine;
public class AppStateManager : MonoBehaviour
{
// 游戏进入后台时调用
void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
Debug.Log("游戏已进入后台,暂停游戏...");
// 在这里处理游戏暂停的操作,例如暂停计时器、停止音效等
Time.timeScale = 0; // 暂停游戏
}
else
{
Debug.Log("游戏已恢复前台,恢复游戏...");
// 在这里恢复游戏的状态,例如重新启用物理、动画等
Time.timeScale = 1; // 恢复游戏
}
}
// 游戏是否获得焦点时调用
void OnApplicationFocus(bool hasFocus)
{
if (hasFocus)
{
Debug.Log("游戏获得焦点,恢复游戏...");
// 恢复游戏
Time.timeScale = 1;
}
else
{
Debug.Log("游戏失去焦点...");
// 可能需要暂停游戏或执行其他操作
Time.timeScale = 0; // 可选择性暂停游戏
}
}
// 游戏退出时调用
void OnApplicationQuit()
{
Debug.Log("游戏退出...");
// 在这里做一些清理工作,例如保存数据等
}
}
将图片的 Texture Type 选项分别选为Texture和Sprite有什么区别
Sprite 作为UI精灵使用,Texture 作用模型贴图使用。Sprite 需要2的整次幂,打包图片省资源。
Unity3d 中的碰撞器和触发器的区别
特性 | 碰撞器(Collider) | 触发器(Trigger) |
---|---|---|
物理响应 | 会产生物理反应(如反弹、摩擦、力的传递等) | 不产生物理反应,只检测进入、离开等事件 |
碰撞检测 | 会触发碰撞事件,物体之间会互相推开、反弹等 | 不会推开物体,仅检测进入、离开事件 |
用途 | 适用于需要物理交互的场景(例如角色碰撞、物体碰撞) | 适用于需要触发事件的场景(例如进入区域触发任务) |
isTrigger | isTrigger 属性为 false(默认值) | isTrigger 属性为 true,将碰撞器设置为触发器 |
事件 | OnCollisionEnter, OnCollisionStay, OnCollisionExit | OnTriggerEnter, OnTriggerStay, OnTriggerExit |
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Enemy"))
{
// 角色与敌人碰撞时,执行一些逻辑
Debug.Log("与敌人发生碰撞!");
}
}
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
// 玩家进入触发器区域,执行一些逻辑
Debug.Log("玩家进入触发区域!");
}
}
当一个细小的高速物体撞向另一个较大物体时,会出现什么情况?如何避免
物体发生碰撞的必要条件是什么
两个物体都必须带有 Collider 碰撞器,其中运动的物体还必须带有 Rigidbody 刚体组件。
CharacterController和 Rigidbody 的区别
Rigidbody具有完全真实物理的特性,Unity中物理系统最基本的一个组件,包含了常用的物理特性,而CharacterController可以说是受限的的Rigidbody,具有一定的物理效果但不是完全真实的,是Unity为了使开发者能方便开发第一人称视角的游戏而封装的一个组件。
什么 Dynamic Font 在 Unicode 环境下优于 Static Font
Unity3d 提供了一个用于保存和读取数据的类,请列出保存和读取整形数据的函数
提示
PlayerPrefs 类可以保存与读取3种基本的数据类型,分别是浮点型、整型、字符串型。
说出你所了解的数据结构,并说明它们的特点
请简述TCP与UDP的区别
列出 Unity3d 提供的几种光源类型
5种光源类型
Directional Light(方向光):方向光模拟的是远处的光源(如太阳或其他恒星)。它的光线是平行的,因此所有照射到物体上的光线方向相同,适用于模拟远距离的光源照射。适用于大范围的场景。
Point Light(点光源):点光源是一种从一个点向四面八方发射光线的光源,类似于电灯泡或火把。光的强度随着距离的增加而衰减。
Spot Light(聚光灯):聚光灯模拟的是从一个点发射的具有特定方向和角度的锥形光束,类似于舞台灯光、车灯等。聚光灯的光照效果是锥形的,随着光源与物体的距离和角度变化,光的强度会变化。
Area Light(区域光):区域光是从一个矩形或正方形的区域发射的光源,光线均匀地从整个区域向外扩散。区域光源适合用于模拟一些大范围的均匀光照,如天花板的灯光。
Light Probes 和 Reflection Probes(光探针和反射探针):光探针是一种特殊的光源类型,用于在场景中存储光照信息。它们本身不发光,但可以用于为动态物体提供精确的光照计算(尤其是在烘焙光照时)。这可以帮助你实现全局光照效果,使动态物体在静态光照环境下看起来更自然;反射探针用于捕捉场景的环境反射信息,并通过反射贴图为物体提供反射效果。它们主要用于模拟反射效果。
为什么建立连接协议是三次握手,而关闭连接却是四次握手呢
TCP 是全双工通信,即双方都能同时发送和接收数据。三次握手的目的是:客户端发送 SYN:向服务器请求建立连接,并告知自己的初始序列号(ISN)。服务器回复 SYN-ACK:确认客户端的 SYN,同时发送自己的 ISN(服务器也需要建立反向连接)。客户端发送 ACK:确认服务器的 SYN,此时双方均确认了对方的发送能力。为什么不是两次?如果只有两次握手,服务器无法确认客户端是否收到了自己的 SYN-ACK(即客户端可能未准备好接收数据),可能导致资源浪费或半连接问题。为什么不是四次?客户端的 ACK 已经隐含了双向通信的确认,无需再额外交互。
关闭连接需要分别终止两个方向的通信(全双工特性),因此需要四次交互:客户端发送 FIN:表示客户端不再发送数据(但仍可接收)。服务器回复 ACK:确认客户端的 FIN,但服务器可能还有数据要发送。服务器发送 FIN:当服务器数据发送完毕后,才发送自己的 FIN。客户端回复 ACK:确认服务器的 FIN,连接彻底关闭。为什么不是三次?服务器的 ACK 和 FIN 不能合并为一次发送,因为中间可能需要时间处理未发送完的数据(即被动关闭方需要等待应用层确认数据已发送完毕)。(如果服务器没有剩余数据,可以合并为三次,但 TCP 协议设计需兼容所有情况。)
列出 Unity3d 生命周期中重要的几个函数
Reset->Awake->OnEnable->Start->FixedUpdate->Update->LateUpdate->OnGUI->OnDisable->OnDestroy。
物理更新和移动摄像机一般放在哪个系统函数里
MeshRender 中 Material 和 Shader 的区别
为何大家都在移动设备上寻求U3D原生GUI的替代方案
不美观,OnGUI很耗费时间,效率不高,使用不方便。
Unity 摄像机有几种工作方式
Unity 中的摄像机(Camera)是游戏场景中的核心组件之一,它负责将游戏世界渲染到屏幕上。Unity 中的摄像机主要有以下几种工作方式:
LayerMask.NameToLayer 这个方法有什么作用
在 Unity 中,LayerMask.NameToLayer()
是一个非常有用的方法,它的作用是将层级名称转换为层级索引。层级索引是一个整数,用于标识场景中的不同层级。
作用:在射线检测(Raycast)、碰撞检测(Collision)等物理操作中或者后处理效果处理上,通常需要指定检测的层级。此时可以使用层级索引来构建。LayerMask.NameToLayer()
是通过名称查找索引的操作,性能开销较小,但仍建议在初始化时缓存结果,避免频繁调用。
// 获取层级索引:
int enemyLayerIndex = LayerMask.NameToLayer("Enemy");
Debug.Log("Enemy Layer Index: " + enemyLayerIndex);
//在射线检测中,可以使用层级索引构建 LayerMask,以过滤特定层级的物体。
int enemyLayerIndex = LayerMask.NameToLayer("Enemy");
if (enemyLayerIndex == -1)
{
Debug.LogError("Enemy layer does not exist!");
return;
}
LayerMask enemyLayerMask = 1 << enemyLayerIndex; // 将层级索引转换为 LayerMask
Ray ray = new Ray(origin, direction);
RaycastHit hit;
if (Physics.Raycast(ray, out hit, Mathf.Infinity, enemyLayerMask))
{
Debug.Log("Hit an enemy: " + hit.collider.name);
}
Material 和 Physic Material 区别
在 Unity 中,Material 和 Physic Material 是两个完全不同的概念,分别用于处理渲染和物理相关的功能。以下是它们的详细区别:
MeshCollider 和其他 Collider 的一个主要不同点
在 Unity 中,MeshCollider 是一种特殊的碰撞器,与其他类型的碰撞器(如 BoxCollider、SphereCollider、CapsuleCollider 等)相比,有一个主要的不同点:MeshCollider 使用网格的几何形状作为碰撞边界,而其他 Collider 使用简单的几何形状(如立方体、球体、胶囊体等)作为碰撞边界。
特性 | MeshCollider | 其他 Collider(Box/Sphere/Capsule) |
---|---|---|
碰撞形状 | 基于网格的复杂形状 | 基于简单几何形状 |
精度 | 高 | 低 |
性能开销 | 高 | 低 |
支持动态物体 | 需要勾选 Convex | 直接支持 |
适用场景 | 静态物体、复杂形状 | 动态物体、简单形状 |
一个角色要用到unity中寻路系统,应该添加哪个组件?NavMeshObstacle组件的作用
NavMeshAgent。NavMeshObstacle组件是寻路网格动态碰撞组件,用于运动的物体阻碍寻路物体效果。
射线中RaycastHit代表什么
射线碰到的碰撞信息。
什么是世界坐标,什么是局部坐标
什么是矢量图
计算机中显示的图形一般可以分为两大类:矢量图和位图。
两种阴影判断的方法与工作原理
在 Unity 中,阴影(Shadows)是实现真实感渲染的一个重要元素。阴影的计算方法直接影响游戏的视觉效果和性能。两种常见的阴影判断方法是基于光源的阴影判断和基于接收物体的阴影判断,其中最常见的两种阴影技术分别是投射阴影(Shadow Casting)和接收阴影(Shadow Receiving)。
基于光源的阴影判断方法(Shadow Casting):这种方法的基本思路是在渲染物体时,根据每个物体与光源之间的关系,判断该物体是否被阴影遮挡。原理如下:
基于接收物体的阴影判断方法(Shadow Receiving):与投射阴影不同,接收阴影的判断方法是根据物体表面的位置与方向,判断该物体是否会接收到光源投射下来的阴影。接收阴影的核心原理是阴影贴图技术。
什么是矩阵?什么是矩阵运算?
矩阵是由一组数字按照矩形排列形成的二维数组。它通常用于表示线性变换、系统的方程、图形变换等。在数学中,矩阵被广泛应用于线性代数、计算机图形学、物理学等领域。
一个矩阵由行(rows)和列(columns)组成,通常用大写字母表示,例如 矩阵的大小通常表示为 (m 行,n 列)。一个 的矩阵,表示为:
矩阵运算是对矩阵进行各种数学操作的集合。常见的矩阵运算有加法、减法、乘法、转置、求逆等。这些运算在处理线性变换、解线性方程组、计算图形变换等方面起到了重要作用。
其中行列式 为:
什么是矢量和标量
主要区别:
矢量运算:
角度和弧度的转换
角度和弧度转换: = (约57.3)
= (约0.017)
在类的静态构造函数前加上修饰符会报错,为什么
静态构造函数,又称类构造函数,是一种特殊的构造函数,不需要显式调用,一般起初始化作用,它是由 CLR 在类第一次被使用时自动调用的。静态构造函数不能有任何访问修饰符或参数。有如下特点:
函数 Func(string a, string b) 用 Lambda 表达式怎么写
数列 1,1,2,3,5,8,13 ...第 n 位数是多少?用 C# 递归算法实现
public static int Add(int num)
{
if(num==0||num==1) return 1;
else return Add(num-2)+Add(num-1);
}
请简述 C++ 与 C# 的区别
C++ 和 C# 都是广泛使用的编程语言,但它们的设计理念、用途和语法有许多不同。
特性 | C++ | C# |
---|---|---|
设计目标 | 高性能、底层控制、系统编程、嵌入式开发 | 企业级应用、跨平台开发、Web 和桌面应用 |
内存管理 | 手动管理内存(new/delete) | 自动垃圾回收(GC) |
语言特性 | 多重继承、指针、模板 | 简洁的语法、垃圾回收、异步编程 (async/await) |
性能 | 高性能,底层操作 | 较高性能,但由于垃圾回收和托管环境,稍逊于 C++ |
平台 | 原生代码,需重新编译支持不同平台 | 跨平台(通过 .NET Core) |
开发工具 | 编译器(如 GCC)和库(如 Boost) | Visual Studio 和 .NET 的丰富工具和框架 |
并发编程 | 原始线程库,较为复杂 | 内置异步支持,简化并发编程 |
学习曲线 | 较陡,学习内存管理和底层概念 | 较平缓,现代化语 |
简言之:C# 是一种完全面向对象的语言,而 C++ 不是。C# 是基于 IL 中间语言和 Net Framework CLR 的,可移植性、可维护性和健壮性都比 C++ 有很大的改进。C# 设计的目的是用来快速开发稳定、可扩展的应用程序,当然也可以通过 interop
和 pinvoke
完成一些底层操作。
请简述 Array、ArrayList 和 List<> 的主要区别
特性 | 数组(Array) | ArrayList | List<> |
---|---|---|---|
大小 | 固定大小 | 动态扩容 | 动态扩容 |
类型安全 | 是 | 否(存储 Object 类型) | 是 |
性能 | 高效 | 较低(装箱/拆箱开销) | 高效(无装箱/拆箱) |
维度 | 多维 | 一维 | 一维 |
继承 | System.Array | System.IList | System.IList |
适用场景 | 数据量固定、高效访问 | 动态大小、非类型安全 | 动态大小、类型安全 |
ref 、out 、params 参数是什么,有什么区别
ref、out 和 params 都是 C# 中的关键字,用来传递方法参数。它们的用途和行为有所不同。
void Increment(ref int x) { x++; // 修改传入的参数 x } int num = 5; Increment(ref num); // 必须先初始化变量 Console.WriteLine(num); // 输出 6
void TryDivide(int numerator, int denominator, out int result) { if (denominator == 0) { result = 0; // 必须初始化 result } else { result = numerator / denominator; } } int res; TryDivide(10, 2, out res); Console.WriteLine(res); // 输出 5
void PrintNumbers(params int[] numbers) { foreach (var num in numbers) { Console.WriteLine(num); } } PrintNumbers(1, 2, 3, 4); // 传递多个参数 PrintNumbers(5); // 传递一个参数 PrintNumbers(); // 不传递任何参数
请描述 Interface 与抽象类之间的不同
// 接口
public interface IDriveable
{
void Start(); // 接口只定义方法签名
void Stop();
}
public class Car : IDriveable
{
public void Start() // 实现接口中的方法
{
Console.WriteLine("Car is starting.");
}
public void Stop()
{
Console.WriteLine("Car is stopping.");
}
}
// 抽象类
public abstract class Vehicle
{
public abstract void Start(); // 抽象方法,要求子类实现
public void Stop() // 具体方法,子类可以继承或重写
{
Console.WriteLine("Vehicle is stopping.");
}
}
public class Car : Vehicle
{
public override void Start() // 实现抽象方法
{
Console.WriteLine("Car is starting.");
}
}
未尽事宜
- 使用规则:
抽象类主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能;
如果要设计大的功能单元,则使用抽象类;如果要设计小而精炼的功能块,则使用接口;
如果预计要创建组件的多个版本,则创建抽象类。接口一旦创建就不能更改。如果需要接口的新版本,必须创建一个全新的接口;
如果创建的功能将在大范围的全异对象间使用,则使用接口;如果要在组件的所有实现间提供通用的已实现功能,则使用抽象类;
分析对象,提炼内部共性形成抽象类,用以表示对象本质,即"是什么"。为外部提供调用或功能需要扩充时优先使用接口;
好的接口定义应该是具有专一功能的,而不能是多功能的,否则造成接口污染。如果一个类只是实现了这个这个接口中的一个功能,而不得不去实现接口中的其他方法,就叫做接口污染;
尽量避免使用继承来实现组件功能,而是使用黑箱复用,即对象组合。因为继承的层次增多,造成最直接的后果就是当你调用这个类群中某一类,就必须把他们全部加载到栈中,后果可想而知!
面向对象思想的一个重要原则就是:面向接口编程;
借助接口和抽象类,23个设计模式中的很多思想被巧妙的实现了,我认为其精髓简单说来就是:面向抽象编程;
抽象类应主要用于关系密切的对象,而接口最适合为不相关的类提供通用功能;
接口侧重于CAN-DO关系类型,而抽象类则侧重于IS-A式关系;
接口多定义对象的行为,抽象类多定义对象的属性;
抽象类定义可以使用public、protected、internal和private修饰符,但是几乎所有的接口都定义为public,原因就不必多说了;
尽量将接口设计成功能单一的模块,以.Net Framwork为例,Idisposable、Idisposable、Icomparable、Iequatable、Ienumerable等都只包含一个公共方法;
接口名称前面的大写字母I是一个约定,正如字段名以下划线开头一样,请坚持这些原则;
如果预计会出现版本问题,可以创建"抽象类"。例如创建了"狗"、"鸡"、"鸭",那么就应该考虑抽象出动物类来应对以后可能出现的风、马、牛的事情。而接口中添加新成员则会强制要求修改所有派生类、并重新编译,所以版本式的问题最好以抽象来实现。
从抽象类派生的非抽象类必须包括继承的所有抽象方法和抽象访问器的实现;
对抽象类不能使用new关键字,也不能被密封,原因是抽象类不能被实例化;
在抽象方法声明中不能使用static或virtual修饰符。
- 共性、个性与选择:
当在差异较大的对象间寻求功能上的共性时,使用接口;当在共性较多的对象间寻求功能上的差异时,使用抽象类。
个性大于共性时;差异较大的个性间具有某些相同的行为;相同行为的实现方式具有较大区别时选择接口;
共性大于个性;共性相同的个体间必然具有相同的属性与行为;相同行为的实现方式具有一定区别时选择抽象类;
下列代码在运行中会发生什么问题?如何避免
List<int> ls = new List<int>(new int[] { 1, 2, 3, 4, 5 });
foreach (int item in ls)
{
Console.WriteLine(item * item);
ls.Remove(item);
}
以上会产生运行时错误,因为 foreach 是只读的。不能一边遍历一边修改。使用 foreach 时候不要对内容进行修改,如果要修改可以使用 for。
下列代码在运行中会产生几个临时对象
string a = new string("abc");
a = (a.ToUpper() + "123").Substring(0, 2);
//第一行是会出错的。应该这样初始化:
string b = new string(new char[]{'a','b','c'});
以上代码会产生3个临时变量。
请简述关键字 Sealed 用在类声明和函数声明时的作用
请简述 Private,Public,Protected,Internal 的区别
面向对象的设计原则,并分别简述它们的含义
总结:开闭原则是目标,里氏替换原则是基础,而依赖倒置原则是手段。他们之间相辅相成,相互补充,共同使我们的软件具有良好的扩展性。
简述C# 委托及用处
委托是一个类,它定义了方法的类型,使得可以将方法当做另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用 if-else(switch)语句,同时使得程序具有更好的扩展性。
微软规范:委托类型的名称都应该以 EventHandler 结束。
微软规范:事件的命名为委托去掉 EventHandler 之后剩余的部分。
微软规范:继承自 EventArgs 的类型应该以 EventArgs 结尾。
请描述你所了解的设计模式,并说明在你的项目中哪里使用过
简述数据结构,并说明它们的特点
当需要频繁创建使用某个对象时,有什么好的程序设计方案来节省内存
设计单例模式进行创建对象或者使用对象池。
JIT 和 AOT 区别是什么
Foreach 循环迭代时,若把其中的某个元素删除,程序报错,怎么找到那个元素?以及具体怎么处理这种情况
foreach 不能进行元素的删除,因为迭代器会锁定迭代的集合。解决方法是记录找到索引或者 key 值,迭代结束后再进行删除。
For,foreach,Enumerator.MoveNext 的使用与内存消耗情况
内存消耗上本质上并没有太大的区别。但是在 Unity 中的 Update 中,一般不推荐使用 foreach 因为会遗留内存垃圾。
C# 中 unsafe 关键字是用来做什么的?什么场合下使用
非托管代码才需要,这个关键字一般用在带指针操作的场合。
C# 中的排序方式有哪些
主要有选择排序,冒泡排序,快速排序,插入排序,希尔排序,归并排序等。
假如有一个Person 类,包含姓名,年龄字段,如对 Person 数组按年龄升序排列,也可以按姓名升序排列,将来可以会有更多的排序方式,你该怎么实现,说出思路。
// 例如:
var arr = arr.OrderBy(p=>p.age).ToArray();
什么是协程函数?
在主线程运行的同时开启另一段逻辑处理,来协助当前程序的执行,协程很像多线程,但是不是多线程,Unity的协程会在每帧结束之后去检测yield的条件是否满足。
C#中四种访问修饰符是哪些?各有什么区别?
1.属性修饰符 2.存取修饰符 3.类修饰符 4.成员修饰符。
详细介绍
属性修饰符:
Serializable:按值将对象封送到远程服务器。
STATread:是单线程套间的意思,是一种线程模型。
MATAThread:是多线程套间的意思,也是一种线程模型。
存取修饰符:
public:存取不受限制。
private:只有包含该成员的类可以存取。
internal:只有当前工程可以存取。
protected:只有包含该成员的类以及派生类可以存取。
类修饰符:
abstract:抽象类。指示一个类只能作为其它类的基类。
sealed:密封类。指示一个类不能被继承。理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。
成员修饰符:
abstract:指示该方法或属性没有实现。
sealed:密封方法。可以防止在派生类中对该方法的override(重载)。不是类的每个成员方法都可以作为密封方法密封方法,必须对基类的虚方法进行重载,提供具体的实现方法。所以,在方法的声明中,sealed修饰符总是和override修饰符同时使用。
delegate:委托。用来定义一个函数指针。C#中的事件驱动是基于delegate + event的。
const:指定该成员的值只读不允许修改。
event:声明一个事件。
extern:指示方法在外部实现。
override:重写。对由基类继承成员的新实现。
readonly:指示一个域只能在声明时以及相同类的内部被赋值。
static:指示一个成员属于类型本身,而不是属于特定的对象。即在定义后可不经实例化,就可使用。
virtual:指示一个方法或存取器的实现可以在继承类中被覆盖。
new:在派生类中隐藏指定的基类成员,从而实现重写的功能。 若要隐藏继承类的成员,请使用相同名称在派生类中声明该成员,并用 new 修饰符修饰它。
MeshRender 中 material 和 sharedmaterial 的区别?
修改 sharedMaterial 将改变所有使用这个材质的物体外观,并且也改变储存在工程里的材质设置。不推荐修改由sharedMaterial返回的材质。如果你想修改渲染器的材质,使用material替代。
概述序列化
序列化简单理解成把对象转换为容易传输的格式的过程。比如,可以序列化一个对象,然后使用HTTP通过Internet在客户端和服务器端之间传输该对象。
认识跳槽
校园招聘 是进入大厂的最好机会,职业生涯有较高的起点。进入 社会 后再想进大厂需要更多年的努力奋斗才有机会。
有工作经验的 跳槽依据 是:能学东西升级自己,能取得较高的报酬(期权股票 + 项目分红),良好的工作环境、氛围、资源与人脉。
一般公司的面试流程
岗位设置
简历
面试官关注的简历有 技术点匹配度、经验匹配度、项目匹配度。
简历格式:基本信息 + 职业技能 + 项目经验 + 个人评价。
以 UNITY 岗位为例的简历格式:
面试官选题依据
注意事项
遇到不会的问题时,诚恳的承认不会。需要时会认真学习研究。不要瞎答。不要说模棱两可的答案。
面试时选定一个目标,然后先投其他公司试炼几次,差不多时再投自己真正的目标公司。这样通过的几率高些。