object click event

This commit is contained in:
azykov@mail.ru 2026-06-05 15:25:52 +03:00
parent 3d518ce3f0
commit 9dbd8d607c
No known key found for this signature in database
26 changed files with 270 additions and 55 deletions

View File

@ -5,6 +5,7 @@ Blockly.Blocks['console_log_action'] = {
init(this: Blockly.Block) { init(this: Blockly.Block) {
this.appendValueInput('VALUE') this.appendValueInput('VALUE')
.setAlign(Blockly.inputs.Align.RIGHT) .setAlign(Blockly.inputs.Align.RIGHT)
.setCheck('ObjectType')
.appendField('print'); .appendField('print');
this.setInputsInline(false) this.setInputsInline(false)
this.setPreviousStatement(true, null); this.setPreviousStatement(true, null);

View File

@ -1 +1,2 @@
export * from './consoleLog'; export * from './consoleLog';
export * from './physics';

View File

@ -0,0 +1,29 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
Blockly.Blocks['physics_apply_impulse_action'] = {
init(this: Blockly.Block) {
this.appendDummyInput('')
.appendField('Push me to');
this.appendValueInput('POSITION')
.setCheck('Pos');
this.appendDummyInput('')
.appendField('with force')
.appendField(new Blockly.FieldNumber(1, -10, 10, 2), 'FORCE');
this.setInputsInline(true)
this.setPreviousStatement(true, null);
this.setNextStatement(true, null);
this.setTooltip('Push me to a position');
this.setColour(150);
}
};
javascriptGenerator.forBlock['physics_apply_impulse_action'] = function (block, generator) {
const positionValue = generator.valueToCode(block, 'POSITION', Order.ATOMIC);
const forceValue = block.getFieldValue('FORCE');
return `api.applyImpulse(${positionValue}, ${forceValue});\n`;
};
export { };

View File

@ -0,0 +1 @@
export * from './applyImpulse';

View File

@ -0,0 +1,18 @@
import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript";
Blockly.Blocks['object_clicked_event'] = {
init(this: Blockly.Block) {
this.appendDummyInput()
.appendField('When I\'m clicked');
this.setStyle('event_hat');
this.setNextStatement(true);
this.setTooltip('Fires when an object is clicked');
}
};
javascriptGenerator.forBlock['object_clicked_event'] = function (_block, _generator) {
return '';
};
export { };

View File

@ -1,3 +1,4 @@
export * from './gameStart'; export * from './gameStart';
export * from './objectTouchedMe'; export * from './objectTouchedMe';
export * from './playerTouchedMe'; export * from './playerTouchedMe';
export * from './imClicked';

View File

@ -1,15 +1,13 @@
import * as Blockly from "blockly"; import * as Blockly from "blockly";
import { javascriptGenerator } from "blockly/javascript"; import { javascriptGenerator } from "blockly/javascript";
import { getObjectTypeDropDownOptions } from "../fieldTypes/objectType";
Blockly.Blocks['object_touch_start_event'] = { Blockly.Blocks['object_touch_start_event'] = {
init(this: Blockly.Block) { init(this: Blockly.Block) {
this.appendDummyInput() this.appendDummyInput('NAME')
.appendField('When') .appendField('When');
.appendField( this.appendValueInput('NAME')
new Blockly.FieldDropdown(getObjectTypeDropDownOptions), .setCheck('Object');
'OBJECT_TYPE', this.appendDummyInput('NAME')
)
.appendField('touches me'); .appendField('touches me');
this.setStyle('event_hat'); this.setStyle('event_hat');
this.setNextStatement(true); this.setNextStatement(true);

View File

@ -0,0 +1 @@
export * from './objectType';

View File

@ -1,11 +1,11 @@
import { state } from "../../state/rootState"; import * as Blockly from "blockly";
import { toJS } from "mobx";
export function getObjectTypeDropDownOptions(): [string, string][] { export class ObjecTypeField extends Blockly.Field {
const types = Object.values(toJS(state.world.data.objectTypes)); constructor(value: any, validator: any) {
const values: [string, string][] = [ super(value, validator);
['Any object', '*'],
...types.map(t => [t.name, t.id] as [string, string]), this.SERIALIZABLE = true;
]; }
return values;
} }
Blockly.fieldRegistry.register('field_object_type', ObjecTypeField);

View File

@ -21,7 +21,16 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string {
//TODO use args //TODO use args
result += `onGameEvent('${block.type.replace(/_event$/, '')}', function(context) {\n${body}});\n`; result += `onGameEvent(
'${block.type.replace(/_event$/, '')}',
function(context) {
// console.dir({context, api, object, objectType});
if (context.object && (context.object.id !== object.id))
return;
${body}
}
);
`;
} }
} }
return result; return result;

View File

@ -3,3 +3,4 @@ export * from './values';
export * from './actions'; export * from './actions';
export * from './theme'; export * from './theme';
export * from './generateCode'; export * from './generateCode';
export * from './fieldTypes';

View File

@ -0,0 +1,21 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
Blockly.Blocks['any_object_of_type_value'] = {
init(this: Blockly.Block) {
this.appendDummyInput('NAME')
.appendField('Any object of type');
this.appendValueInput('NAME')
.setCheck('ObjectType');
this.setInputsInline(true);
this.setOutput(true, 'Object');
this.setTooltip('Any object of type');
this.setColour(315);
}
};
javascriptGenerator.forBlock['any_object_of_type_value'] = function (_block, _generator) {
return ['123', Order.NONE];
};
export { };

View File

@ -4,16 +4,16 @@ import { javascriptGenerator, Order } from "blockly/javascript";
Blockly.Blocks['current_object_value'] = { Blockly.Blocks['current_object_value'] = {
init(this: Blockly.Block) { init(this: Blockly.Block) {
this.appendEndRowInput('NAME') this.appendEndRowInput('NAME')
.appendField('Current object'); .appendField('Me');
this.setInputsInline(false) this.setInputsInline(false)
this.setOutput(true, 'String'); this.setOutput(true, 'Object');
this.setTooltip('Returns id of current object instance'); this.setTooltip('Returns current object instance');
this.setColour(315); this.setColour(315);
} }
}; };
javascriptGenerator.forBlock['current_object_value'] = function (_block, _generator) { javascriptGenerator.forBlock['current_object_value'] = function (_block, _generator) {
return ['object.id', Order.NONE]; return ['object', Order.NONE];
}; };
export { }; export { };

View File

@ -4,16 +4,16 @@ import { javascriptGenerator, Order } from "blockly/javascript";
Blockly.Blocks['current_object_type_value'] = { Blockly.Blocks['current_object_type_value'] = {
init(this: Blockly.Block) { init(this: Blockly.Block) {
this.appendEndRowInput('NAME') this.appendEndRowInput('NAME')
.appendField('Current object type'); .appendField('My type');
this.setInputsInline(false) this.setInputsInline(false)
this.setOutput(true, 'String'); this.setOutput(true, 'ObjectType');
this.setTooltip('Returns id of current object type'); this.setTooltip('Returns current object type');
this.setColour(315); this.setColour(315);
} }
}; };
javascriptGenerator.forBlock['current_object_type_value'] = function (_block, _generator) { javascriptGenerator.forBlock['current_object_type_value'] = function (_block, _generator) {
return ['objectType.id', Order.NONE]; return ['objectType', Order.NONE];
}; };
export { }; export { };

View File

@ -1,2 +1,6 @@
export * from './currentObject'; export * from './currentObject';
export * from './currentObjectType'; export * from './currentObjectType';
export * from './anyObjectOfType';
export * from './objectType';
export * from './positionOfObject';
export * from './position';

View File

@ -0,0 +1,33 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
import { toJS } from "mobx";
import { state } from "../../state";
export function getObjectTypeDropDownOptions(): [string, string][] {
const types = Object.values(toJS(state.world.data.objectTypes));
const values: [string, string][] = [
['Any object', '*'],
...types.map(t => [t.name, t.id] as [string, string]),
];
return values;
}
Blockly.Blocks['object_type_value'] = {
init(this: Blockly.Block) {
this.appendEndRowInput('NAME')
.appendField(
new Blockly.FieldDropdown(getObjectTypeDropDownOptions),
'OBJECT_TYPE',
);
this.setInputsInline(false);
this.setOutput(true, 'ObjectType');
this.setTooltip('Object type');
this.setColour(315);
}
};
javascriptGenerator.forBlock['current_object_type_value'] = function (_block, _generator) {
return ['objectType', Order.NONE];
};
export { };

View File

@ -0,0 +1,29 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
Blockly.Blocks['pos_value'] = {
init(this: Blockly.Block) {
this.appendDummyInput('POSITION')
.appendField('[')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 2), 'X')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 2), 'Y')
.appendField(',')
.appendField(new Blockly.FieldNumber(0, -Infinity, Infinity, 2), 'Z');
this.appendDummyInput()
.appendField(']');
this.setInputsInline(true);
this.setOutput(true, 'Pos');
this.setTooltip('Position [x,y,z]');
this.setColour(315);
}
};
javascriptGenerator.forBlock['pos_value'] = function (block, _generator) {
const xValue = block.getFieldValue('X');
const yValue = block.getFieldValue('Y');
const zValue = block.getFieldValue('Z');
return [`[${xValue},${yValue},${zValue}]`, Order.ATOMIC];
};
export { };

View File

@ -0,0 +1,21 @@
import * as Blockly from "blockly";
import { javascriptGenerator, Order } from "blockly/javascript";
Blockly.Blocks['position_of_object_value'] = {
init(this: Blockly.Block) {
this.appendDummyInput('NAME')
.appendField('Position of');
this.appendValueInput('NAME')
.setCheck('Object');
this.setInputsInline(true);
this.setOutput(true, 'Pos');
this.setTooltip('Position of an object');
this.setColour(315);
}
};
javascriptGenerator.forBlock['position_of_object_value'] = function (_block, _generator) {
return ['123', Order.NONE];
};
export { };

View File

@ -1,11 +1,11 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import type { ObjectInstance, Runtime, RuntimeGameObjectInstance } from "../types"; import type { RuntimeGameObjectInstance } from "../types";
import { state } from "../state"; import { state } from "../state";
import { ObjectViewInternal } from "./ObjectViewInternal"; import { ObjectViewInternal } from "./ObjectViewInternal";
import { PlayerObjectView } from "./CharacterView"; import { PlayerObjectView } from "./CharacterView";
export type GameObjectViewProps = { export type GameObjectViewProps = {
object: Runtime<ObjectInstance>; object: RuntimeGameObjectInstance;
isPlayer: boolean; isPlayer: boolean;
} }
@ -17,5 +17,13 @@ export const GameObjectView = observer(function (props: GameObjectViewProps) {
if (props.isPlayer) if (props.isPlayer)
return <PlayerObjectView object={props.object as RuntimeGameObjectInstance} objectType={objectType} />; return <PlayerObjectView object={props.object as RuntimeGameObjectInstance} objectType={objectType} />;
return <ObjectViewInternal {...props} objectType={objectType} />; function handleClick(): void {
state.game?.emitEvent('object_clicked', { object: props.object });
}
return <ObjectViewInternal
{...props}
objectType={objectType}
onClick={handleClick}
/>;
}); });

View File

@ -60,16 +60,15 @@ export const ObjectEditorView = observer(function ({ object, isPlayer }: ObjectE
if (!objectType) if (!objectType)
return null; return null;
function handleClick(e: ThreeEvent<MouseEvent>) { function handleClick() {
if (e.delta > 5) return; if (state.worldEditor.isEnabled) {
e.stopPropagation();
// Reading selection state inside an event handler: not tracked by observer. // Reading selection state inside an event handler: not tracked by observer.
const currentMode = state.worldEditor.isEnabled && const currentMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id
state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id
? state.worldEditor.selection?.editMode ? state.worldEditor.selection?.editMode
: undefined; : undefined;
state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) }); state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) });
} }
}
function handleTransformEnd() { function handleTransformEnd() {
const group = dataRef.current?.group; const group = dataRef.current?.group;

View File

@ -18,7 +18,7 @@ type ObjectViewInternalProps = {
objectType: ObjectType; objectType: ObjectType;
isEditor?: boolean; isEditor?: boolean;
isPlayer?: boolean; isPlayer?: boolean;
onClick?: (e: ThreeEvent<MouseEvent>) => void; onClick?: () => void;
ref?: Ref<ObjectViewInternalHandle>; ref?: Ref<ObjectViewInternalHandle>;
} }
@ -56,6 +56,37 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
[object.id] [object.id]
); );
useEffect(
() => {
const gameObj = object as RuntimeGameObjectInstance;
return reaction(
() => gameObj.pendingActions.impulse,
(impulse) => {
if (!impulse) return;
if (!rbRef.current) { console.warn('applyImpulse: rbRef is null for', gameObj.id); return; }
const { direction, amplitude } = impulse;
const v = { x: direction[0] * amplitude, y: direction[1] * amplitude, z: direction[2] * amplitude };
console.log('applyImpulse', gameObj.id, v, 'bodyType', rbRef.current.bodyType());
rbRef.current.applyImpulse(v, true);
runInAction(() => {
gameObj.pendingActions.impulse = undefined;
});
},
);
},
[object.id]
);
function handleClick(e: ThreeEvent<MouseEvent>) {
if (e.delta > 5)
return;
e.stopPropagation();
props.onClick?.();
}
return ( return (
<SyncRigidBody <SyncRigidBody
ref={rbRef} ref={rbRef}
@ -80,7 +111,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
ref={groupRef} ref={groupRef}
name={`${object.id} (${objectType.id} instance)`} name={`${object.id} (${objectType.id} instance)`}
scale={object.scale} scale={object.scale}
onClick={props.onClick} onClick={handleClick}
> >
{ {
object.cache.voxelGroups.map((vg) => object.cache.voxelGroups.map((vg) =>

View File

@ -19,6 +19,7 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
{ kind: 'block', type: 'game_start_event' }, { kind: 'block', type: 'game_start_event' },
{ kind: 'block', type: 'object_touch_start_event' }, { kind: 'block', type: 'object_touch_start_event' },
{ kind: 'block', type: 'player_touch_start_event' }, { kind: 'block', type: 'player_touch_start_event' },
{ kind: 'block', type: 'object_clicked_event' },
], ],
}, },
{ {
@ -28,6 +29,10 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
contents: [ contents: [
{ kind: 'block', type: 'current_object_value' }, { kind: 'block', type: 'current_object_value' },
{ kind: 'block', type: 'current_object_type_value' }, { kind: 'block', type: 'current_object_type_value' },
{ kind: 'block', type: 'object_type_value' },
{ kind: 'block', type: 'any_object_of_type_value' },
{ kind: 'block', type: 'position_of_object_value' },
{ kind: 'block', type: 'pos_value' },
], ],
}, },
{ {
@ -37,6 +42,7 @@ const TOOLBOX: Blockly.utils.toolbox.ToolboxDefinition = {
contents: [ contents: [
{ kind: 'block', type: 'console_log_action' }, { kind: 'block', type: 'console_log_action' },
{ kind: 'block', type: 'text_print' }, { kind: 'block', type: 'text_print' },
{ kind: 'block', type: 'physics_apply_impulse_action' },
], ],
}, },
{ {

View File

@ -79,13 +79,13 @@ export class GameFactory {
Object.values(world.objectTypes) Object.values(world.objectTypes)
.filter((ot) => ot.javascript) .filter((ot) => ot.javascript)
.forEach((ot) => { .forEach((ot) => {
const otCode = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`); const gameScript = eval(`(function gameScript({onGameEvent, offGameEvent }, {api, object, objectType}) {${ot.javascript}})`);
Object.values(scene.objects) Object.values(scene.objects)
.filter((o) => o.typeId == ot.id) .filter((o) => o.typeId == ot.id)
.forEach((o) => { .forEach((o) => {
const api = new ObjectApi(o, ot, world, scene); const api = new ObjectApi(o, ot, world, scene);
otCode(internalBus, { api, o, ot }); gameScript(internalBus, { api, object: o, objectType: ot });
}) })
}); });
} }

View File

@ -3,6 +3,7 @@ import type { WorldState } from "./worldState";
import type { Game, Pos3, RuntimeGameScene } from "../types"; import type { Game, Pos3, RuntimeGameScene } from "../types";
import type { CameraProps } from "@react-three/fiber"; import type { CameraProps } from "@react-three/fiber";
import { GameEventBus, GameFactory } from "../model/gameFactory"; import { GameEventBus, GameFactory } from "../model/gameFactory";
import type { GameEventContext } from "../types/runtime/gameBus";
export class GameState { export class GameState {
private readonly world: WorldState; private readonly world: WorldState;
@ -45,18 +46,18 @@ export class GameState {
const eventBus = new GameEventBus(); const eventBus = new GameEventBus();
this.eventBus = eventBus; this.eventBus = eventBus;
GameFactory.evalGameScripts(rawWorld, game.scene, eventBus);
this._stopAutoSave = this.startAutoSave(); this._stopAutoSave = this.startAutoSave();
makeAutoObservable( makeAutoObservable(this);
this,
); GameFactory.evalGameScripts(rawWorld, this.scene, eventBus);
} }
public emitEvent(event: string) { public emitEvent(event: string, context: Omit<GameEventContext, 'world' | 'scene'> = {}) {
this.eventBus.emit( this.eventBus.emit(
event, event,
{ {
...context,
world: this.world.data, world: this.world.data,
scene: this.scene, scene: this.scene,
}, },

View File

@ -1,17 +1,12 @@
import type { ObjectType, RuntimeGameObjectInstance, Scene, World } from "../model"; import type { RuntimeGameObjectInstance, Scene, World } from "../model";
export type GameEventContext = { export type GameEventContext = {
world: World; world: World;
scene: Scene; scene: Scene;
object?: RuntimeGameObjectInstance;
} }
// export type GameObjectEventContext = GameEventContext & {
// object: RuntimeGameObjectInstance;
// objectType: ObjectType;
// }
export type GameEventHandler = (context: GameEventContext) => void; export type GameEventHandler = (context: GameEventContext) => void;
// export type GameObjectEventHandler = (context: GameObjectEventContext) => void;
export type InternalGameEventBus = { export type InternalGameEventBus = {
onGameEvent: (event: string, handler: GameEventHandler) => void; onGameEvent: (event: string, handler: GameEventHandler) => void;

View File

@ -18,7 +18,14 @@ export class ObjectApi {
this.scene = scene; this.scene = scene;
} }
public applyImpulse(direction: V3, amplitude: number) { public applyImpulse(targetPosition: V3, amplitude: number) {
const [px, py, pz] = this.object.position;
const dx = targetPosition[0] - px;
const dy = targetPosition[1] - py;
const dz = targetPosition[2] - pz;
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len < 1e-6) return;
const direction: V3 = [dx / len, dy / len, dz / len];
this.object.pendingActions.impulse = { direction, amplitude }; this.object.pendingActions.impulse = { direction, amplitude };
} }
} }