mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-25 17:10:43 +00:00
wip
This commit is contained in:
parent
1a5a4c834f
commit
5cefdd224b
9 changed files with 197 additions and 28 deletions
|
|
@ -173,6 +173,15 @@ export interface ChatEventTypes {
|
|||
};
|
||||
}
|
||||
|
||||
export interface WorldRoomEventTypes {
|
||||
enter: {
|
||||
user: Packed<'UserLite'>;
|
||||
};
|
||||
left: {
|
||||
userId: MiUser['id'];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ReversiEventTypes {
|
||||
matched: {
|
||||
game: Packed<'ReversiGameDetailed'>;
|
||||
|
|
@ -315,6 +324,10 @@ export type GlobalEvents = {
|
|||
name: `chatRoomStream:${MiChatRoom['id']}`;
|
||||
payload: EventTypesToEventPayload<ChatEventTypes>;
|
||||
};
|
||||
worldRoom: {
|
||||
name: `worldRoomStream:${string}`;
|
||||
payload: EventTypesToEventPayload<WorldRoomEventTypes>;
|
||||
};
|
||||
reversi: {
|
||||
name: `reversiStream:${MiUser['id']}`;
|
||||
payload: EventTypesToEventPayload<ReversiEventTypes>;
|
||||
|
|
@ -435,4 +448,9 @@ export class GlobalEventService {
|
|||
public publishReversiGameStream<K extends keyof ReversiGameEventTypes>(gameId: MiReversiGame['id'], type: K, value?: ReversiGameEventTypes[K]): void {
|
||||
this.publish(`reversiGameStream:${gameId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public publishWorldRoomStream<K extends keyof WorldRoomEventTypes>(roomId: string, type: K, value?: WorldRoomEventTypes[K]): void {
|
||||
this.publish(`worldRoomStream:${roomId}`, type, typeof value === 'undefined' ? null : value);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ import { IdService } from '@/core/IdService.js';
|
|||
import type { MiUser } from '@/models/User.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
type PlayerState = {
|
||||
position: [number, number, number],
|
||||
|
|
@ -39,11 +42,15 @@ export class WorldRoomMultiplayService {
|
|||
private roleService: RoleService,
|
||||
private queryService: QueryService,
|
||||
private idService: IdService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async enter(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('enter', { userId, roomId });
|
||||
|
||||
// TODO: atomicにやる
|
||||
const currentPlayers = await this.redisClient.hlen(`worldRoom:${roomId}:players`);
|
||||
if (currentPlayers < 10) {
|
||||
|
|
@ -54,6 +61,11 @@ export class WorldRoomMultiplayService {
|
|||
} else {
|
||||
throw new Error('Room is full.');
|
||||
}
|
||||
|
||||
// TODO: 既に入っていたらスキップ
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'enter', {
|
||||
user: await this.userEntityService.pack(userId),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -70,11 +82,17 @@ export class WorldRoomMultiplayService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async leave(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
public async left(userId: MiUser['id'], roomId: MiWorldRoom['id']) {
|
||||
console.log('left', { userId, roomId });
|
||||
|
||||
const redisPipeline = this.redisClient.pipeline();
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:players`, userId);
|
||||
redisPipeline.hdel(`worldRoom:${roomId}:playerStates`, userId);
|
||||
await redisPipeline.exec();
|
||||
|
||||
this.globalEventService.publishWorldRoomStream(roomId, 'left', {
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -97,4 +115,29 @@ export class WorldRoomMultiplayService {
|
|||
this.heartbeat(userId, roomId);
|
||||
return this.getPlayerStates(roomId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packPlayerProfile(user: Packed<'UserLite'>) {
|
||||
return {
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
avatarUrl: user.avatarUrl,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getPlayerProfiles(roomId: MiWorldRoom['id'], userId?: MiUser['id']): Promise<Record<string, any>> {
|
||||
let playerIds = await this.redisClient.hkeys(`worldRoom:${roomId}:players`);
|
||||
playerIds = playerIds.filter(id => id !== userId);
|
||||
|
||||
const packedUsers = await this.userEntityService.packMany(playerIds);
|
||||
|
||||
const profiles: Record<string, any> = {};
|
||||
for (const playerId of playerIds) {
|
||||
const packedUser = packedUsers.find(u => u.id === playerId);
|
||||
if (packedUser == null) continue;
|
||||
profiles[playerId] = this.packPlayerProfile(packedUser);
|
||||
}
|
||||
return profiles;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class WorldRoomChannel extends Channel {
|
|||
return false;
|
||||
}
|
||||
|
||||
//this.subscriber.on(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
this.subscriber.on(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -62,19 +62,37 @@ export class WorldRoomChannel extends Channel {
|
|||
|
||||
this.isEntered = true;
|
||||
|
||||
this.send('entered', {});
|
||||
this.send('entered', {
|
||||
playerProfiles: await this.worldRoomMultiplayService.getPlayerProfiles(this.roomId, this.user!.id),
|
||||
});
|
||||
|
||||
this.intervalId = setInterval(async () => {
|
||||
const states = await this.worldRoomMultiplayService.getPlayerStatesAndHeatbeat(this.user!.id, this.roomId);
|
||||
// TODO: 自分自身のstateは抜く
|
||||
delete states[this.user!.id];
|
||||
this.send('sync', states);
|
||||
}, 1000);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
//@bindThis
|
||||
//private async onEvent(data: GlobalEvents['worldRoom']['payload']) {
|
||||
// this.send(data.type, data.body);
|
||||
//}
|
||||
@bindThis
|
||||
private async onEvent(data: GlobalEvents['worldRoom']['payload']) {
|
||||
switch (data.type) {
|
||||
case 'enter': {
|
||||
if (data.body.user.id === this.user!.id) return; // 自分の入室は無視
|
||||
this.send('playerEntered', {
|
||||
id: data.body.user.id,
|
||||
profile: this.worldRoomMultiplayService.packPlayerProfile(data.body.user.id),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'left': {
|
||||
if (data.body.userId === this.user!.id) return; // 自分の退室は無視
|
||||
this.send('playerLeft', {
|
||||
id: data.body.userId,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onMessage(type: string, body: any) {
|
||||
|
|
@ -89,9 +107,9 @@ export class WorldRoomChannel extends Channel {
|
|||
|
||||
@bindThis
|
||||
public dispose() {
|
||||
//this.subscriber.off(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
this.subscriber.off(`worldRoomStream:${this.roomId}`, this.onEvent);
|
||||
|
||||
clearInterval(this.intervalId);
|
||||
this.worldRoomMultiplayService.leave(this.user!.id, this.roomId);
|
||||
this.worldRoomMultiplayService.left(this.user!.id, this.roomId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { WORLD_SCALE } from 'misskey-world/src/utility.js';
|
|||
|
||||
export type PlayerProfile = {
|
||||
name: string;
|
||||
username: string;
|
||||
avatarUrl: string;
|
||||
};
|
||||
|
||||
|
|
@ -65,7 +66,8 @@ export class PlayerContainer {
|
|||
this.root.position.set(...state.position);
|
||||
this.root.rotation.set(...state.rotation);
|
||||
if (!forInit) {
|
||||
this.sr.updateMesh(this.root.getChildMeshes());
|
||||
const meshes = this.root.getChildMeshes();
|
||||
if (meshes.length > 0) this.sr.updateMesh(meshes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1452,20 +1452,23 @@ export class RoomEngine extends EngineBase<{
|
|||
this.ev('playSfxUrl', { url, options });
|
||||
}
|
||||
|
||||
public updatePlayers(profiles: Record<string, PlayerProfile>, states: Record<string, PlayerState>) {
|
||||
public updatePlayerProfiles(profiles: Record<string, PlayerProfile>) {
|
||||
this.playerProfiles = profiles;
|
||||
|
||||
for (const playerContainer of this.playerContainers) {
|
||||
if (profiles[playerContainer.id] == null) {
|
||||
if (this.playerProfiles[playerContainer.id] == null) {
|
||||
this.sr.disableSnapshotRendering();
|
||||
playerContainer.destroy();
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
}
|
||||
this.playerContainers = this.playerContainers.filter(p => profiles[p.id] != null);
|
||||
this.playerContainers = this.playerContainers.filter(p => this.playerProfiles[p.id] != null);
|
||||
}
|
||||
|
||||
for (const [k, v] of Object.entries(profiles)) {
|
||||
public updatePlayerStates(states: Record<string, PlayerState>) {
|
||||
for (const [k, v] of Object.entries(this.playerProfiles)) {
|
||||
const playerContainer = this.playerContainers.find(p => p.id === k);
|
||||
if (playerContainer == null) {
|
||||
this.sr.disableSnapshotRendering();
|
||||
const p = new PlayerContainer({
|
||||
id: k,
|
||||
profile: v,
|
||||
|
|
@ -1473,7 +1476,56 @@ export class RoomEngine extends EngineBase<{
|
|||
scene: this.scene,
|
||||
sr: this.sr,
|
||||
});
|
||||
this.sr.enableSnapshotRendering();
|
||||
// TODO: loadObjectのものとある程度共通化
|
||||
p.registerMeshes = (meshes) => {
|
||||
for (const mesh of meshes) {
|
||||
if (SYSTEM_MESH_NAMES.some(n => mesh.name.includes(n))) {
|
||||
mesh.receiveShadows = false;
|
||||
mesh.isVisible = false;
|
||||
} else {
|
||||
mesh.receiveShadows = true;
|
||||
// TODO: メモリリークしそうだからいい感じにする
|
||||
this.envManager.addShadowCaster(mesh);
|
||||
|
||||
//if (mesh.material) (mesh.material as BABYLON.PBRMaterial).ambientColor = new BABYLON.Color3(0.2, 0.2, 0.2);
|
||||
if (mesh.material) {
|
||||
if (mesh.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of mesh.material.subMaterials) {
|
||||
if ((subMat as BABYLON.PBRMaterial).subSurface.isRefractionEnabled) {
|
||||
(subMat as BABYLON.PBRMaterial).subSurface.isRefractionEnabled = false; // 有効にするとドローコールが激増する
|
||||
(subMat as BABYLON.PBRMaterial).transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND;
|
||||
(subMat as BABYLON.PBRMaterial).alpha = 0.5;
|
||||
(subMat as BABYLON.PBRMaterial).metallic = 1;
|
||||
}
|
||||
(subMat as BABYLON.PBRMaterial).reflectionTexture = this.envManager?.envMapIndoor;
|
||||
if ((subMat as BABYLON.PBRMaterial).metadata == null) (subMat as BABYLON.PBRMaterial).metadata = {};
|
||||
(subMat as BABYLON.PBRMaterial).metadata.useEnvMapAsObjectMaterial = true;
|
||||
(subMat as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
(subMat as BABYLON.PBRMaterial).anisotropy.isEnabled = false; // なんかきれいにレンダリングされないため
|
||||
}
|
||||
} else {
|
||||
if ((mesh.material as BABYLON.PBRMaterial).subSurface.isRefractionEnabled) {
|
||||
(mesh.material as BABYLON.PBRMaterial).subSurface.isRefractionEnabled = false; // 有効にするとドローコールが激増する
|
||||
(mesh.material as BABYLON.PBRMaterial).transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND;
|
||||
(mesh.material as BABYLON.PBRMaterial).alpha = 0.5;
|
||||
(mesh.material as BABYLON.PBRMaterial).metallic = 1;
|
||||
}
|
||||
(mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envManager?.envMapIndoor;
|
||||
if ((mesh.material as BABYLON.PBRMaterial).metadata == null) (mesh.material as BABYLON.PBRMaterial).metadata = {};
|
||||
(mesh.material as BABYLON.PBRMaterial).metadata.useEnvMapAsObjectMaterial = true;
|
||||
(mesh.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
(mesh.material as BABYLON.PBRMaterial).anisotropy.isEnabled = false; // なんかきれいにレンダリングされないため
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.scene.meshes.includes(mesh)) this.scene.addMesh(mesh);
|
||||
}
|
||||
};
|
||||
p.loadAvatar().then(() => {
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.sr.enableSnapshotRendering();
|
||||
});
|
||||
this.playerContainers.push(p);
|
||||
} else {
|
||||
if (states[k] != null) {
|
||||
|
|
|
|||
|
|
@ -286,7 +286,7 @@ const roomControllerOptions = computed<RoomControllerOptions>(() => ({
|
|||
}));
|
||||
|
||||
const controller = markRaw(new RoomController(deepClone(initialRoomState), roomControllerOptions.value));
|
||||
const multiplayer = markRaw(new Multiplayer(props.room.id));
|
||||
const multiplayer = markRaw(new Multiplayer(props.room.id, controller));
|
||||
|
||||
onMounted(async () => {
|
||||
// TODO: babylonに依存しないで判定する
|
||||
|
|
@ -349,7 +349,7 @@ onMounted(async () => {
|
|||
|
||||
useInterval(() => {
|
||||
multiplayer.updateState(controller.myPlayerState.value);
|
||||
}, 1000, { immediate: false, afterMounted: true });
|
||||
}, 100, { immediate: false, afterMounted: true });
|
||||
|
||||
onDeactivated(() => {
|
||||
controller.destroy();
|
||||
|
|
@ -657,7 +657,7 @@ function showOtherMenu(ev: PointerEvent) {
|
|||
}
|
||||
|
||||
function leaveOnline() {
|
||||
multiplayer.leave();
|
||||
multiplayer.left();
|
||||
}
|
||||
|
||||
function enterOnline() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type { ShallowRef } from 'vue';
|
|||
import type { RoomStateObject } from 'misskey-world/src/room/object.js';
|
||||
import type { RoomEngine } from 'misskey-world-engine/src/room/engine.js';
|
||||
import type { RoomAttachments, RoomState } from 'misskey-world/src/room/type.js';
|
||||
import type { PlayerState } from 'misskey-world-engine/src/PlayerContainer.js';
|
||||
import type { PlayerProfile, PlayerState } from 'misskey-world-engine/src/PlayerContainer.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import { deepEqual } from '@/utility/deep-equal.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
|
@ -222,4 +222,16 @@ export class RoomController extends EngineControllerBase<RoomEngine> {
|
|||
public standUp() {
|
||||
this.call('standUp');
|
||||
}
|
||||
|
||||
public updatePlayerProfiles(profiles: Record<string, PlayerProfile>) {
|
||||
this.call('updatePlayerProfiles', [profiles]);
|
||||
}
|
||||
|
||||
public updatePlayerStates(states: Record<string, PlayerState>) {
|
||||
this.call('updatePlayerStates', [states]);
|
||||
}
|
||||
|
||||
public clearPlayers() {
|
||||
this.call('clearPlayers');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
import { reactive, ref, shallowRef, triggerRef, watch } from 'vue';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { PlayerState } from 'misskey-world-engine/src/PlayerContainer.js';
|
||||
import type { PlayerProfile, PlayerState } from 'misskey-world-engine/src/PlayerContainer.js';
|
||||
import type { RoomController } from './controller.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import * as os from '@/os.js';
|
||||
import { withTimeout } from '@/utility/promise-timeout.js';
|
||||
|
|
@ -14,13 +15,18 @@ import { deepEqual } from '@/utility/deep-equal.js';
|
|||
|
||||
export class Multiplayer {
|
||||
public isOnline = ref(false);
|
||||
private controller: RoomController;
|
||||
private connection: Misskey.IChannelConnection<Misskey.Channels['worldRoom']> | null = null;
|
||||
private roomId: string;
|
||||
private playerProfiles: Record<string, PlayerProfile> = {};
|
||||
|
||||
constructor(roomId: string) {
|
||||
constructor(roomId: string, controller: RoomController) {
|
||||
this.roomId = roomId;
|
||||
this.controller = controller;
|
||||
|
||||
this.onSync = this.onSync.bind(this);
|
||||
this.onPlayerEntered = this.onPlayerEntered.bind(this);
|
||||
this.onPlayerLeft = this.onPlayerLeft.bind(this);
|
||||
}
|
||||
|
||||
public enter() {
|
||||
|
|
@ -28,8 +34,13 @@ export class Multiplayer {
|
|||
this.connection = useStream().useChannel('worldRoom', {
|
||||
roomId: this.roomId,
|
||||
});
|
||||
this.connection.once('entered', () => {
|
||||
this.connection.once('entered', ({ playerProfiles }) => {
|
||||
console.log('entered', playerProfiles);
|
||||
this.playerProfiles = playerProfiles;
|
||||
this.controller.updatePlayerProfiles(this.playerProfiles);
|
||||
this.connection!.on('sync', this.onSync);
|
||||
this.connection!.on('playerEntered', this.onPlayerEntered);
|
||||
this.connection!.on('playerLeft', this.onPlayerLeft);
|
||||
this.isOnline.value = true;
|
||||
resolve();
|
||||
});
|
||||
|
|
@ -42,7 +53,7 @@ export class Multiplayer {
|
|||
});
|
||||
}
|
||||
|
||||
public leave() {
|
||||
public left() {
|
||||
if (this.connection == null) return;
|
||||
this.connection.dispose();
|
||||
this.connection = null;
|
||||
|
|
@ -61,9 +72,20 @@ export class Multiplayer {
|
|||
|
||||
private onSync(states: Record<string, PlayerState>) {
|
||||
console.log('sync', states);
|
||||
this.controller.updatePlayerStates(states);
|
||||
}
|
||||
|
||||
private onPlayerEntered(data: { id: string; profile: PlayerProfile; }) {
|
||||
this.playerProfiles[data.id] = data.profile;
|
||||
this.controller.updatePlayerProfiles(this.playerProfiles);
|
||||
}
|
||||
|
||||
private onPlayerLeft(data: { id: string; }) {
|
||||
delete this.playerProfiles[data.id];
|
||||
this.controller.updatePlayerProfiles(this.playerProfiles);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.leave();
|
||||
this.left();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -295,8 +295,10 @@ export type Channels = {
|
|||
roomId: string;
|
||||
};
|
||||
events: {
|
||||
entered: () => void;
|
||||
entered: (payload: { playerProfiles: any; }) => void;
|
||||
sync: (payload: any) => void;
|
||||
playerEntered: (payload: { id: string; profile: any; }) => void;
|
||||
playerLeft: (payload: { id: string; }) => void;
|
||||
};
|
||||
receives: {
|
||||
update: any;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue