Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/examples/with-game-example-video.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/qr-codes/with-game-qr-code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions with-game/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

.granite
*.ait
.pnp.*
.yarn/
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
7 changes: 7 additions & 0 deletions with-game/.yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
enableGlobalCache: false
cacheFolder: .yarn/cache
npmScopes:
toss-design-system:
npmRegistryServer: 'https://registry.npmjs.org'
npmAlwaysAuth: true
npmAuthToken: 'NPM_TOKEN'
47 changes: 47 additions & 0 deletions with-game/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Example Game

![WebView](../assets/tags/tag-webview.svg.svg)
![Toss App](../assets/tags/tag-toss-app.svg)
![Sandbox App](../assets/tags/tag-sandbox-app.svg)

`@apps-in-toss/web-framework`를 활용해 React와 Three.js로 만든 게임 예제에요.
이 예제에서는 사운드, 가로 모드, 게임 프로필, 리더보드 등 게임 개발에 필요한 다양한 요소들을 참고할 수 있어요.

- [**사운드**](https://developers-apps-in-toss.toss.im/checklist/app-game.html#_3-사운드): 배경음, 효과음, 햅틱 등을 적용하는 예시를 볼 수 있어요.
- [**가로 모드**](https://developers-apps-in-toss.toss.im/bedrock/reference/framework/%ED%99%94%EB%A9%B4%20%EC%A0%9C%EC%96%B4/setDeviceOrientation.html#setdeviceorientation): setDeviceOrientation을 사용해 가로 화면으로 전환하는 방법을 확인할 수 있어요.
- [**게임 프로필 & 리더보드**](https://developers-apps-in-toss.toss.im/development/leaderboard.html): 전체 랭킹을 확인하고, 친구를 추가하거나 친구에게 내 점수를 자랑할 수 있어요.

<img src="../assets/examples/with-game-example-video.gif" alt="example gif" width="700px" />

<br />

## 📲 체험하기

<img src="../assets/qr-codes/with-game-qr-code.svg" ait="qr code" width="100px" />&nbsp;

<br />

## 🚀 설치 및 실행 방법

1. **ZIP 파일**을 다운로드하고 압축을 풀어주세요.

2. 필요한 패키지를 설치해요.

```
yarn install
```

3. 개발 서버를 실행해요.
\*iOS 실기기로 테스트 시 [실기기에서 개발 모드 사용하기](https://developers-apps-in-toss.toss.im/tutorials/webview.html#%E1%84%89%E1%85%B5%E1%86%AF%E1%84%80%E1%85%B5%E1%84%80%E1%85%B5%E1%84%8B%E1%85%A6%E1%84%89%E1%85%A5-%E1%84%80%E1%85%A2%E1%84%87%E1%85%A1%E1%86%AF-%E1%84%86%E1%85%A9%E1%84%83%E1%85%B3-%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%92%E1%85%A1%E1%84%80%E1%85%B5)를 참고하여 host 설정을 해주세요.
```
yarn dev
```

<br />

## 📌 참고사항

- [WebView 개발하기](https://developers-apps-in-toss.toss.im/tutorials/webview.html)
- [사운드](https://developers-apps-in-toss.toss.im/checklist/app-game.html#_3-사운드)
- [setDeviceOrientation](https://developers-apps-in-toss.toss.im/bedrock/reference/framework/%ED%99%94%EB%A9%B4%20%EC%A0%9C%EC%96%B4/setDeviceOrientation.html#setdeviceorientation)
- [게임 프로필 & 리더보드](https://developers-apps-in-toss.toss.im/development/leaderboard.html)
23 changes: 23 additions & 0 deletions with-game/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'

export default tseslint.config([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
25 changes: 25 additions & 0 deletions with-game/granite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { defineConfig } from '@apps-in-toss/web-framework/config';

export default defineConfig({
appName: 'with-game',
brand: {
displayName: '게임 예제', // 화면에 노출될 앱의 한글 이름으로 바꿔주세요.
primaryColor: '#3182F6', // 화면에 노출될 앱의 기본 색상으로 바꿔주세요.
icon: 'https://static.toss.im/appsintoss/73/1414e0f9-f3eb-4b56-a138-e3351502738d.png', // 화면에 노출될 앱의 아이콘 이미지 주소로 바꿔주세요.
bridgeColorMode: 'basic',
},
web: {
host: '10.242.116.80',
port: 5173,
commands: {
dev: 'vite --host',
build: 'tsc -b && vite build',
},
},
permissions: [],
outdir: 'dist',
webViewProps: {
type: 'game',
overScrollMode: 'never',
},
});
18 changes: 18 additions & 0 deletions with-game/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>With Game</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Joti+One&family=Rubik:[email protected]&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
50 changes: 50 additions & 0 deletions with-game/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "with-game",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "granite dev",
"build": "granite build",
"lint": "eslint .",
"preview": "vite preview",
"deploy": "ait deploy"
},
"dependencies": {
"@apps-in-toss/web-framework": "1.0.1",
"@granite-js/plugin-router": "0.1.21",
"@react-three/drei": "^10.6.1",
"@react-three/fiber": "^9.3.0",
"@react-three/rapier": "^2.1.0",
"@tanstack/react-router": "^1.130.8",
"@types/howler": "^2.2.12",
"clsx": "^2.1.1",
"howler": "^2.2.4",
"leva": "^0.10.0",
"maath": "^0.10.8",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"three": "^0.178.0",
"three-stdlib": "^2.36.0",
"zustand": "^5.0.7"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
"@gltf-transform/cli": "^4.2.1",
"@granite-js/plugin-router": "0.1.21",
"@types/node": "^24.1.0",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/three": "^0",
"@vitejs/plugin-react-swc": "^3.10.2",
"eslint": "^9.30.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.3.0",
"r3f-perf": "^7.2.3",
"typescript": "~5.8.3",
"typescript-eslint": "^8.35.1",
"vite": "^7.0.4"
},
"packageManager": "[email protected]"
}
Binary file added with-game/public/bg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added with-game/public/bgm.wav
Binary file not shown.
Binary file added with-game/public/bottle-cap.glb
Binary file not shown.
Binary file added with-game/public/hit-sound.wav
Binary file not shown.
Binary file added with-game/public/miss-sound.wav
Binary file not shown.
Binary file added with-game/public/room.glb
Binary file not shown.
Binary file added with-game/public/score-sound.wav
Binary file not shown.
Binary file added with-game/public/title-bg.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions with-game/src/components/BGM.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useEffect } from 'react';
import { useAudio } from '@/hooks/useAudio';

interface BGMProps {
isGameEnd: boolean;
}

export function BGM({ isGameEnd }: BGMProps) {
const { playAudio, stopAudio, fade } = useAudio({
src: '/bgm.wav',
loop: true,
volume: 0.1,
});

useEffect(() => {
playAudio();
return () => {
stopAudio();
};
}, [playAudio, stopAudio]);

useEffect(() => {
if (isGameEnd) {
fade(0.1, 0.0, 1500);
return;
}
fade(0.0, 0.1, 1500);
}, [isGameEnd, fade]);

return null;
}
27 changes: 27 additions & 0 deletions with-game/src/components/BackgroundPlane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Image } from '@react-three/drei';

interface BackgroundPlaneProps {
url?: string;
filterColor?: string;
filterOpacity?: number;
}

export function BackgroundPlane({
url = '/bg.jpg',
filterColor = '#000000',
filterOpacity = 0.3,
}: BackgroundPlaneProps) {
return (
<group rotation={[Math.PI / 2, 0, 0]} position={[0, 255, 50]} scale={185}>
<Image url={url} />
<mesh position={[0, 0.01, 0]}>
<planeGeometry args={[1, 1]} />
<meshBasicMaterial
color={filterColor}
transparent
opacity={filterOpacity}
/>
</mesh>
</group>
);
}
16 changes: 16 additions & 0 deletions with-game/src/components/CameraRig.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PerspectiveCamera, OrbitControls } from '@react-three/drei';

export function CameraRig() {
return (
<group>
<PerspectiveCamera
makeDefault
position={[0, -20.5, 3]}
zoom={1.5}
near={0.1}
far={1000}
/>
<OrbitControls enabled={false} />
</group>
);
}
28 changes: 28 additions & 0 deletions with-game/src/components/Cap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { forwardRef } from 'react';
import { RigidBody, RapierRigidBody } from '@react-three/rapier';
import { useGameAssets } from '@/hooks/useGameAssets';
import { GAME_CONFIG } from '@/utils/gameConfig';
import { useTableStateStore } from '@/store/tableStateStore';

export const Cap = forwardRef<RapierRigidBody>((_, ref) => {
const { scene } = useGameAssets({ path: '/bottle-cap.glb' });
const { tableStartY } = useTableStateStore();

return (
<RigidBody
ref={ref}
position={[
GAME_CONFIG.CAP.START_X,
tableStartY + 0.5,
GAME_CONFIG.CAP.START_Z,
]}
rotation={[Math.PI / 2, 0, 0]}
colliders="cuboid"
scale={0.3}
linearDamping={1}
restitution={0.6}
>
<primitive object={scene} />
</RigidBody>
);
});
Loading