You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity Photon PUN2 玩家生成报错 空引用与预制体加载失败排查

问题描述

开发Unity多人游戏时参考Blackthornprod的多人教程实现Photon 2玩家生成逻辑,出现玩家无法正常生成的问题,运行时报错如下:

  • 启动游戏直接抛出NullReferenceException: Object reference not set to an instance of an object,报错位置为PlayerSpawner.Start () (at Assets/PlayerSpawner.cs:18)
  • 点击方向箭头切换玩家头像时新增两类报错:
    • DefaultPool failed to load "Player". Make sure it's in a "Resources" folder. Or use a custom IPunPrefabPool.(UnityEngine.Debug:LogError (object))
    • NullReferenceException空引用错误,报错位置为PlayerSpawner.Start () (at Assets/PlayerSpawner.cs:20)

相关代码

所有代码、技术术语、API标识均保持原样未修改:

玩家生成逻辑脚本 PlayerSpawner.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;

public class PlayerSpawner : MonoBehaviour
{
public GameObject[] playerPrefabs;
public Transform[] spawnPoints;
public PlayerItem _playerItem;

private void Start()
{
    int randomNumber = Random.Range(0, spawnPoints.Length);
    Debug.Log("1");
    Transform spawnPoint = spawnPoints[randomNumber];
    Debug.Log("2");
    GameObject playerToSpawn = playerPrefabs[(int)PhotonNetwork.LocalPlayer.CustomProperties["playerAvatar"]];
    Debug.Log("3");
    PhotonNetwork.Instantiate(playerToSpawn.name, spawnPoint.position, Quaternion.identity);
    Debug.Log("4");
}
}

玩家头像选择/房间玩家信息脚本 PlayerItem.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Photon.Pun;
using Photon.Realtime;

public class PlayerItem : MonoBehaviourPunCallbacks
{

public Text playerName;

public Color highlightColor;
public GameObject leftArrowButton;
public GameObject rightArrowButton;

ExitGames.Client.Photon.Hashtable playerProperties = new ExitGames.Client.Photon.Hashtable();
public Image playerAvatar;
public GameObject[] avatars;

Player player;

private void Start()
{
}

public void SetPlayerInfo(Player _player)
{
    playerName.text = _player.NickName;
    player = _player;
    UpdatePlayerItem(player);
}

public void ApplyLocalChanges()
{
    leftArrowButton.SetActive(true);
    rightArrowButton.SetActive(true);
}

public void OnClickLeftArrow()
{
    if ((int)playerProperties["playerAvatar"] == 0)
    {
        playerProperties["playerAvatar"] = avatars.Length - 1;
    }
    else
    {
        playerProperties["playerAvatar"] = (int)playerProperties["playerAvatar"] - 1;
    }
    PhotonNetwork.SetPlayerCustomProperties(playerProperties);
}

public void OnClickRightArrow()
{
    if ((int)playerProperties["playerAvatar"] == avatars.Length - 1)
    {
        playerProperties["playerAvatar"] = 0;
    }
    else
    {
        playerProperties["playerAvatar"] = (int)playerProperties["playerAvatar"] + 1;
    }
    PhotonNetwork.SetPlayerCustomProperties(playerProperties);
}

public override void OnPlayerPropertiesUpdate(Player targetPlayer, ExitGames.Client.Photon.Hashtable changedProps)
{
    if (player == targetPlayer)
    {
        UpdatePlayerItem(targetPlayer);
    }
}

void UpdatePlayerItem(Player player)
{
    if (player.CustomProperties.ContainsKey("playerAvatar"))
    {
        playerAvatar.sprite = avatars[(int)player.CustomProperties["playerAvatar"]].GetComponent<SpriteRenderer>().sprite;
        playerProperties["playerAvatar"] = (int)player.CustomProperties["playerAvatar"];
    }
    else
    {
        playerProperties["playerAvatar"] = 0;
    }
}
}

大厅管理/场景跳转脚本 LobbyManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine.UI;

public class LobbyManager : MonoBehaviourPunCallbacks
{
public InputField roomInputField;
public GameObject lobbyPanel;
public GameObject roomPanel;
public Text roomName;

public RoomItem roomItemPrefab;
public List<RoomItem> roomItemList = new List<RoomItem>();
public Transform contentObject;

public float timeBetweenUpdates = 1.5f;
float nextUpdateTime;

public List<PlayerItem> playerItemsList = new List<PlayerItem>();
public PlayerItem playerItemPrefab;
public Transform playerItemParent;

public GameObject playButton;

private void Start()
{
    PhotonNetwork.JoinLobby();
}

public void OnClickCreate()
{
    if (roomInputField.text.Length >= 1)
    {
        PhotonNetwork.CreateRoom(roomInputField.text, new RoomOptions() { MaxPlayers = 10, BroadcastPropsChangeToAll = true });
    }
}

public override void OnJoinedRoom()
{
    lobbyPanel.SetActive(false);
    roomPanel.SetActive(true);
    roomName.text = "Room: " + PhotonNetwork.CurrentRoom.Name;
    UpdatePlayerList();
}

public override void OnRoomListUpdate(List<RoomInfo> roomList)
{
    if (Time.time >= nextUpdateTime)
    {
        UpdateRoomList(roomList);
        nextUpdateTime = Time.time + timeBetweenUpdates;
    }
}

void UpdateRoomList(List<RoomInfo> list)
{
    foreach (RoomItem item in roomItemList)
    {
        Destroy(item.gameObject);
    }
    roomItemList.Clear();

    foreach (RoomInfo room in list)
    {
        RoomItem newRoom = Instantiate(roomItemPrefab, contentObject);
        newRoom.SetRoomName(room.Name);
        roomItemList.Add(newRoom);
    }
}

public void JoinRoom(string roomName)
{
    PhotonNetwork.JoinRoom(roomName);
}

public void OnClickLeaveRoom()
{
    PhotonNetwork.LeaveRoom();
}

public override void OnLeftRoom()
{
    lobbyPanel.SetActive(true);
    roomPanel.SetActive(false);
}

public override void OnConnectedToMaster()
{
    PhotonNetwork.JoinLobby();
}

void UpdatePlayerList()
{
    foreach (PlayerItem item in playerItemsList)
    {
        Destroy(item.gameObject);
    }
    playerItemsList.Clear();

    if (PhotonNetwork.CurrentRoom == null)
    {
        return;
    }
    
    foreach (KeyValuePair<int, Player> player in PhotonNetwork.CurrentRoom.Players)
    {
        PlayerItem newPlayerItem = Instantiate(playerItemPrefab, playerItemParent);
        newPlayerItem.SetPlayerInfo(player.Value);

        if (player.Value == PhotonNetwork.LocalPlayer)
        {
            newPlayerItem.ApplyLocalChanges();
        }

        playerItemsList.Add(newPlayerItem);
    }
}

public override void OnPlayerEnteredRoom(Player newPlayer)
{
    UpdatePlayerList();
}

public override void OnPlayerLeftRoom(Player otherPlayer)
{
    UpdatePlayerList();
}

private void Update()
{
    if (PhotonNetwork.IsMasterClient && PhotonNetwork.CurrentRoom.PlayerCount >= 1)
    {
        playButton.SetActive(true);
    }
    else
    {
        playButton.SetActive(false);
    }
}

public void OnClickPlayButton()
{
    PhotonNetwork.LoadLevel("Game");
}
}

问题排查与修复方案

三个报错对应三个独立问题,按顺序修复即可:

  1. 第18行启动时空引用
    对应代码行是Transform spawnPoint = spawnPoints[randomNumber];,问题是Inspector面板中PlayerSpawner组件的spawnPoints数组未赋值,或数组内存在空元素。修复方式是在Unity编辑器中选中挂载PlayerSpawner的物体,把场景里所有生成点拖到spawnPoints数组中,确认没有空引用。
  2. DefaultPool加载"Player"失败
    Photon 2的PhotonNetwork.Instantiate方法默认只能加载存放在Resources文件夹下的预制体,且预制体名称必须和代码传入的字符串完全一致。修复方式:
    • 在项目Assets目录下新建名为Resources的文件夹(大小写完全匹配)
    • 把playerPrefabs数组引用的所有玩家预制体都放到这个Resources文件夹内
    • 确认每个玩家预制体的名称和数组内引用的物体名完全一致,不要出现代码传"Player"但预制体实际命名为其他值的情况
  3. 第20行切换头像时空引用
    对应代码行是GameObject playerToSpawn = playerPrefabs[(int)PhotonNetwork.LocalPlayer.CustomProperties["playerAvatar"]];,问题有两个:一是playerPrefabs数组未在Inspector赋值,二是玩家刚进入游戏场景时playerAvatar自定义属性未完成初始化,直接强转取值会返回空。修复分两步:
    • 在Inspector给PlayerSpawner的playerPrefabs数组按头像索引顺序赋值所有玩家预制体,数组长度要和PlayerItem里的avatars数组长度完全一致
    • 修改PlayerSpawner.cs的Start逻辑,增加属性存在判断、索引越界兜底,修改后代码如下:
private void Start()
{
    // 校验生成点配置
    if (spawnPoints == null || spawnPoints.Length == 0)
    {
        Debug.LogError("未配置生成点");
        return;
    }
    int randomNumber = Random.Range(0, spawnPoints.Length);
    Transform spawnPoint = spawnPoints[randomNumber];
    
    // 兜底玩家头像索引,默认取0
    int avatarIndex = 0;
    if (PhotonNetwork.LocalPlayer.CustomProperties.ContainsKey("playerAvatar"))
    {
        avatarIndex = (int)PhotonNetwork.LocalPlayer.CustomProperties["playerAvatar"];
        // 防止索引越界
        avatarIndex = Mathf.Clamp(avatarIndex, 0, playerPrefabs.Length - 1);
    }
    
    GameObject playerToSpawn = playerPrefabs[avatarIndex];
    PhotonNetwork.Instantiate(playerToSpawn.name, spawnPoint.position, Quaternion.identity);
}

额外隐藏问题:当前PlayerItem.cs里点击左右箭头切换头像时,没有给playerProperties做初始值兜底,第一次点击时如果playerAvatar属性不存在,强转也会触发空引用,可以在Start方法里加一行playerProperties["playerAvatar"] = 0;做初始值。


内容的提问来源于stack exchange,提问作者Rusty Lloyd Santos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 15:15:31