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) {
this.appendValueInput('VALUE')
.setAlign(Blockly.inputs.Align.RIGHT)
.setCheck('ObjectType')
.appendField('print');
this.setInputsInline(false)
this.setPreviousStatement(true, null);

View File

@ -1 +1,2 @@
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 './objectTouchedMe';
export * from './playerTouchedMe';
export * from './imClicked';

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import { state } from "../../state/rootState";
import { toJS } from "mobx";
import * as Blockly from "blockly";
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;
export class ObjecTypeField extends Blockly.Field {
constructor(value: any, validator: any) {
super(value, validator);
this.SERIALIZABLE = true;
}
}
Blockly.fieldRegistry.register('field_object_type', ObjecTypeField);

View File

@ -21,7 +21,16 @@ export function generateCode(workspace: Blockly.WorkspaceSvg): string {
//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;

View File

@ -3,3 +3,4 @@ export * from './values';
export * from './actions';
export * from './theme';
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'] = {
init(this: Blockly.Block) {
this.appendEndRowInput('NAME')
.appendField('Current object');
.appendField('Me');
this.setInputsInline(false)
this.setOutput(true, 'String');
this.setTooltip('Returns id of current object instance');
this.setOutput(true, 'Object');
this.setTooltip('Returns current object instance');
this.setColour(315);
}
};
javascriptGenerator.forBlock['current_object_value'] = function (_block, _generator) {
return ['object.id', Order.NONE];
return ['object', Order.NONE];
};
export { };

View File

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

View File

@ -1,2 +1,6 @@
export * from './currentObject';
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 type { ObjectInstance, Runtime, RuntimeGameObjectInstance } from "../types";
import type { RuntimeGameObjectInstance } from "../types";
import { state } from "../state";
import { ObjectViewInternal } from "./ObjectViewInternal";
import { PlayerObjectView } from "./CharacterView";
export type GameObjectViewProps = {
object: Runtime<ObjectInstance>;
object: RuntimeGameObjectInstance;
isPlayer: boolean;
}
@ -17,5 +17,13 @@ export const GameObjectView = observer(function (props: GameObjectViewProps) {
if (props.isPlayer)
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)
return null;
function handleClick(e: ThreeEvent<MouseEvent>) {
if (e.delta > 5) return;
e.stopPropagation();
function handleClick() {
if (state.worldEditor.isEnabled) {
// Reading selection state inside an event handler: not tracked by observer.
const currentMode = state.worldEditor.isEnabled &&
state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id
const currentMode = state.worldEditor.selection?.type === 'object' && state.worldEditor.selection?.id === object.id
? state.worldEditor.selection?.editMode
: undefined;
state.worldEditor.setSelectedObject({ id: object.id, editMode: nextObjectEditMode(currentMode) });
}
}
function handleTransformEnd() {
const group = dataRef.current?.group;

View File

@ -18,7 +18,7 @@ type ObjectViewInternalProps = {
objectType: ObjectType;
isEditor?: boolean;
isPlayer?: boolean;
onClick?: (e: ThreeEvent<MouseEvent>) => void;
onClick?: () => void;
ref?: Ref<ObjectViewInternalHandle>;
}
@ -56,6 +56,37 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
[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 (
<SyncRigidBody
ref={rbRef}
@ -80,7 +111,7 @@ export const ObjectViewInternal = function ({ object, objectType, ref, ...props
ref={groupRef}
name={`${object.id} (${objectType.id} instance)`}
scale={object.scale}
onClick={props.onClick}
onClick={handleClick}
>
{
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: 'object_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: [
{ kind: 'block', type: 'current_object_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: [
{ kind: 'block', type: 'console_log_action' },
{ 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)
.filter((ot) => ot.javascript)
.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)
.filter((o) => o.typeId == ot.id)
.forEach((o) => {
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 { CameraProps } from "@react-three/fiber";
import { GameEventBus, GameFactory } from "../model/gameFactory";
import type { GameEventContext } from "../types/runtime/gameBus";
export class GameState {
private readonly world: WorldState;
@ -45,18 +46,18 @@ export class GameState {
const eventBus = new GameEventBus();
this.eventBus = eventBus;
GameFactory.evalGameScripts(rawWorld, game.scene, eventBus);
this._stopAutoSave = this.startAutoSave();
makeAutoObservable(
this,
);
makeAutoObservable(this);
GameFactory.evalGameScripts(rawWorld, this.scene, eventBus);
}
public emitEvent(event: string) {
public emitEvent(event: string, context: Omit<GameEventContext, 'world' | 'scene'> = {}) {
this.eventBus.emit(
event,
{
...context,
world: this.world.data,
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 = {
world: World;
scene: Scene;
object?: RuntimeGameObjectInstance;
}
// export type GameObjectEventContext = GameEventContext & {
// object: RuntimeGameObjectInstance;
// objectType: ObjectType;
// }
export type GameEventHandler = (context: GameEventContext) => void;
// export type GameObjectEventHandler = (context: GameObjectEventContext) => void;
export type InternalGameEventBus = {
onGameEvent: (event: string, handler: GameEventHandler) => void;

View File

@ -18,7 +18,14 @@ export class ObjectApi {
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 };
}
}