Как построить многопользовательскую игру ARCore, используя Cloud Anchor и Sceneform? - PullRequest
0 голосов
/ 26 октября 2018

В настоящее время я изучаю ARCore и Cloud Anchors. Мне удалось создать одно приложение, используя Sceneform и Cloud Anchors. По сути, мы можем разместить один объект на любой поверхности, используя device-1, а device-2 может поделиться тем же опытом, используя Cloud Anchors.

  • После того, как мы разрешим Cloud Anchor, мы не сможем прослушать какие-либо изменения в преобразовании объекта, т.е. если пользователь вращает или перемещает объект на device-1, у нас нет никакой информации о это на device-2

  • Если мы хотим создать многопользовательский режим с помощью Cloud Anchor, единственный найденный мной способ - это синхронизировать состояние объекта с сервером и обоими устройствами.

Мои вопросы:

  • Это правильный способ реализации многопользовательской синхронизации в ARCore?

  • Какие свойства нам нужно синхронизировать, если этот метод возможен?

1 Ответ

0 голосов
/ 16 января 2019

Для создания многопользовательского приложения с использованием технологии Cloud Anchors используйте следующий проект для начальной точки:

package com.google.ar.core.codelab.cloudanchor;

public class MainActivity extends AppCompatActivity implements GLSurfaceView.Renderer {

  private static final String TAG = MainActivity.class.getSimpleName();
  private GLSurfaceView surfaceView;
  private final BackgroundRenderer backgroundRenderer = new BackgroundRenderer();
  private final ObjectRenderer virtualObject = new ObjectRenderer();
  private final ObjectRenderer virtualObjectShadow = new ObjectRenderer();
  private final PlaneRenderer planeRenderer = new PlaneRenderer();
  private final PointCloudRenderer pointCloudRenderer = new PointCloudRenderer();
  private final float[] anchorMatrix = new float[16];
  private final float[] projectionMatrix = new float[16];
  private final float[] viewMatrix = new float[16];
  private final float[] colorCorrectionRgba = new float[4];
  private final Object singleTapAnchorLock = new Object();

  @GuardedBy("singleTapAnchorLock")
  private MotionEvent queuedSingleTap;
  private final SnackbarHelper snackbarHelper = new SnackbarHelper();
  private GestureDetector gestureDetector;
  private DisplayRotationHelper displayRotationHelper;
  private Session session;
  private boolean installRequested;

  @Nullable
  @GuardedBy("singleTapAnchorLock")
  private Anchor anchor;

  private void handleTapOnDraw(TrackingState currentTrackingState, Frame currentFrame) {
    synchronized (singleTapAnchorLock) {
      if (anchor == null
          && queuedSingleTap != null
          && currentTrackingState == TrackingState.TRACKING) {
        for (HitResult hit : currentFrame.hitTest(queuedSingleTap)) {
          if (shouldCreateAnchorWithHit(hit)) {
            Anchor newAnchor = hit.createAnchor();
            setNewAnchor(newAnchor);
            break;
          }
        }
      }
      queuedSingleTap = null;
    }
  }

  private static boolean shouldCreateAnchorWithHit(HitResult hit) {
    Trackable trackable = hit.getTrackable();
    if (trackable instanceof Plane) {
      return ((Plane) trackable).isPoseInPolygon(hit.getHitPose());
    } else if (trackable instanceof Point) {
      return ((Point) trackable).getOrientationMode() == OrientationMode.ESTIMATED_SURFACE_NORMAL;
    }
    return false;
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    surfaceView = findViewById(R.id.surfaceview);
    displayRotationHelper = new DisplayRotationHelper(/*context=*/ this);

    gestureDetector =
        new GestureDetector(
            this,
            new GestureDetector.SimpleOnGestureListener() {
              @Override
              public boolean onSingleTapUp(MotionEvent e) {
                synchronized (singleTapAnchorLock) {
                  queuedSingleTap = e;
                }
                return true;
              }

              @Override
              public boolean onDown(MotionEvent e) {
                return true;
              }
            });
    surfaceView.setOnTouchListener((unusedView, event) -> gestureDetector.onTouchEvent(event));
    surfaceView.setPreserveEGLContextOnPause(true);
    surfaceView.setEGLContextClientVersion(2);
    surfaceView.setEGLConfigChooser(8, 8, 8, 8, 16, 0);
    surfaceView.setRenderer(this);
    surfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
    installRequested = false;

    Button clearButton = findViewById(R.id.clear_button);
    clearButton.setOnClickListener(
        (unusedView) -> {
          synchronized (singleTapAnchorLock) {
            setNewAnchor(null);
          }
        });
  }

  @Override
  protected void onResume() {
    super.onResume();

    if (session == null) {
      Exception exception = null;
      int messageId = -1;
      try {
        switch (ArCoreApk.getInstance().requestInstall(this, !installRequested)) {
          case INSTALL_REQUESTED:
            installRequested = true;
            return;
          case INSTALLED:
            break;
        }

        if (!CameraPermissionHelper.hasCameraPermission(this)) {
          CameraPermissionHelper.requestCameraPermission(this);
          return;
        }
        session = new Session(this);
      } catch (UnavailableArcoreNotInstalledException e) {
        messageId = R.string.snackbar_arcore_unavailable;
        exception = e;
      } catch (UnavailableApkTooOldException e) {
        messageId = R.string.snackbar_arcore_too_old;
        exception = e;
      } catch (UnavailableSdkTooOldException e) {
        messageId = R.string.snackbar_arcore_sdk_too_old;
        exception = e;
      } catch (Exception e) {
        messageId = R.string.snackbar_arcore_exception;
        exception = e;
      }

      if (exception != null) {
        snackbarHelper.showError(this, getString(messageId));
        Log.e(TAG, "Exception creating session", exception);
        return;
      }

      Config config = new Config(session);
      session.configure(config);
    }

    try {
      session.resume();
    } catch (CameraNotAvailableException e) {
      snackbarHelper.showError(this, getString(R.string.snackbar_camera_unavailable));
      session = null;
      return;
    }
    surfaceView.onResume();
    displayRotationHelper.onResume();
  }

  @Override
  public void onPause() {
    super.onPause();
    if (session != null) {
      displayRotationHelper.onPause();
      surfaceView.onPause();
      session.pause();
    }
  }

  @Override
  public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] results) {
    if (!CameraPermissionHelper.hasCameraPermission(this)) {
      Toast.makeText(this, "Camera permission is needed to run this application", Toast.LENGTH_LONG)
          .show();
      if (!CameraPermissionHelper.shouldShowRequestPermissionRationale(this)) {
        CameraPermissionHelper.launchPermissionSettings(this);
      }
      finish();
    }
  }

  @Override
  public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    FullScreenHelper.setFullScreenOnWindowFocusChanged(this, hasFocus);
  }

  @Override
  public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    GLES20.glClearColor(0.1f, 0.1f, 0.1f, 1.0f);

    try {
      backgroundRenderer.createOnGlThread(/*context=*/ this);
      planeRenderer.createOnGlThread(/*context=*/ this, "models/trigrid.png");
      pointCloudRenderer.createOnGlThread(/*context=*/ this);

      virtualObject.createOnGlThread(/*context=*/ this, "models/andy.obj", "models/andy.png");
      virtualObject.setMaterialProperties(0.0f, 2.0f, 0.5f, 6.0f);

      virtualObjectShadow.createOnGlThread(
          /*context=*/ this, "models/andy_shadow.obj", "models/andy_shadow.png");
      virtualObjectShadow.setBlendMode(BlendMode.Shadow);
      virtualObjectShadow.setMaterialProperties(1.0f, 0.0f, 0.0f, 1.0f);
    } catch (IOException ex) {
      Log.e(TAG, "Failed to read an asset file", ex);
    }
  }

  @Override
  public void onSurfaceChanged(GL10 gl, int width, int height) {
    displayRotationHelper.onSurfaceChanged(width, height);
    GLES20.glViewport(0, 0, width, height);
  }

  @Override
  public void onDrawFrame(GL10 gl) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

    if (session == null) {
      return;
    }
    displayRotationHelper.updateSessionIfNeeded(session);

    try {
      session.setCameraTextureName(backgroundRenderer.getTextureId());
      Frame frame = session.update();
      Camera camera = frame.getCamera();
      TrackingState cameraTrackingState = camera.getTrackingState();

      handleTapOnDraw(cameraTrackingState, frame);

      backgroundRenderer.draw(frame);

      if (cameraTrackingState == TrackingState.PAUSED) {
        return;
      }

      camera.getProjectionMatrix(projectionMatrix, 0, 0.1f, 100.0f);
      camera.getViewMatrix(viewMatrix, 0);

      PointCloud pointCloud = frame.acquirePointCloud();
      pointCloudRenderer.update(pointCloud);
      pointCloudRenderer.draw(viewMatrix, projectionMatrix);

      pointCloud.release();

      planeRenderer.drawPlanes(
          session.getAllTrackables(Plane.class), camera.getDisplayOrientedPose(), projectionMatrix);

      boolean shouldDrawAnchor = false;
      synchronized (singleTapAnchorLock) {
        if (anchor != null && anchor.getTrackingState() == TrackingState.TRACKING) {
          frame.getLightEstimate().getColorCorrection(colorCorrectionRgba, 0);

          anchor.getPose().toMatrix(anchorMatrix, 0);
          shouldDrawAnchor = true;
        }
      }
      if (shouldDrawAnchor) {
        float scaleFactor = 1.0f;
        frame.getLightEstimate().getColorCorrection(colorCorrectionRgba, 0);

        virtualObject.updateModelMatrix(anchorMatrix, scaleFactor);
        virtualObjectShadow.updateModelMatrix(anchorMatrix, scaleFactor);
        virtualObject.draw(viewMatrix, projectionMatrix, colorCorrectionRgba);
        virtualObjectShadow.draw(viewMatrix, projectionMatrix, colorCorrectionRgba);
      }
    } catch (Throwable t) {
      Log.e(TAG, "Exception on the OpenGL thread", t);
    }
  }

  @GuardedBy("singleTapAnchorLock")
  private void setNewAnchor(@Nullable Anchor newAnchor) {
    if (anchor != null) {
      anchor.detach();
    }
    anchor = newAnchor;
  }
}

... а вот класс StorageManager:

package com.google.ar.core.codelab.cloudanchor;

class StorageManager {

    interface CloudAnchorIdListener {
        void onCloudAnchorIdAvailable(String cloudAnchorId);
    }

    interface ShortCodeListener {
        void onShortCodeAvailable(Integer shortCode);
    }

    private static final String TAG = StorageManager.class.getName();
    private static final String KEY_ROOT_DIR = "shared_anchor_codelab_root";
    private static final String KEY_NEXT_SHORT_CODE = "next_short_code";
    private static final String KEY_PREFIX = "anchor;";
    private static final int INITIAL_SHORT_CODE = 142;
    private final DatabaseReference rootRef;

    StorageManager(Context context) {
        FirebaseApp firebaseApp = FirebaseApp.initializeApp(context);
        rootRef = FirebaseDatabase.getInstance(firebaseApp).getReference().child(KEY_ROOT_DIR);
        DatabaseReference.goOnline();
    }

    void nextShortCode(ShortCodeListener listener) {
        rootRef
        .child(KEY_NEXT_SHORT_CODE)
        .runTransaction(
                        new Transaction.Handler() {
            @Override
            public Transaction.Result doTransaction(MutableData currentData) {
                Integer shortCode = currentData.getValue(Integer.class);
                if (shortCode == null) {
                    shortCode = INITIAL_SHORT_CODE - 1;
                }
                currentData.setValue(shortCode + 1);
                return Transaction.success(currentData);
            }

            @Override
            public void onComplete(
                                   DatabaseError error, boolean committed, DataSnapshot currentData) {
                if (!committed) {
                    Log.e(TAG, "Firebase Error", error.toException());
                    listener.onShortCodeAvailable(null);
                } else {
                    listener.onShortCodeAvailable(currentData.getValue(Integer.class));
                }
            }
        });
    }

    void storeUsingShortCode(int shortCode, String cloudAnchorId) {
        rootRef.child(KEY_PREFIX + shortCode).setValue(cloudAnchorId);
    }

    void getCloudAnchorID(int shortCode, CloudAnchorIdListener listener) {
        rootRef
        .child(KEY_PREFIX + shortCode)
        .addListenerForSingleValueEvent(
                                        new ValueEventListener() {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                listener.onCloudAnchorIdAvailable(String.valueOf(dataSnapshot.getValue()));
            }

            @Override
            public void onCancelled(DatabaseError error) {
                Log.e(TAG, "The database operation for getCloudAnchorID was cancelled.",
                      error.toException());
                listener.onCloudAnchorIdAvailable(null);
            }
        });
    }
}

Надеюсь, это поможет.

...